User Generated Data Process
Table of Contents:
- Overview
- User Workflow
- Required Structure
- Metadata Files
- Generate Item Metadata
- Regenerate Item Metadata in S3
- Maintainer Notes
- Optional S3 Upload
- Runtime Initialization
- Troubleshooting
- External References
This project helps you prepare a User Generated collection for DestinE Data Lake review.
Use this guide to:
- Structure your collection folders and data assets.
- Configure metadata files.
- Generate STAC Item metadata.
For contributor and maintenance documentation, see docs/development.md.
User Workflow
- Create your collection root folder using the provided collection id, for example
EO.XXX.YYY.ZZZ. - If needed, place the collection in a subfolder under your current working directory and pass that subfolder with
--collection-path. - Add metadata files under
metadata/. - Organize data assets under
data/according to your configured folder level. - Run generate_item_metadata.py to validate and generate STAC Items under
metadata/items/.
If you mirror a collection from S3 before regenerating items, mirrored data folders may contain usergenerated_s3_folder_manifest.jsonl files instead of placeholder dummy assets. Item generation detects these manifests automatically and uses their entries as the authoritative asset list.
Reference collection structure is available in EO.XXX.YYY.ZZZ.
Required Structure
Expected top-level structure:
EO.XXX.YYY.ZZZ/
data/
metadata/
collection.json
collection_config.json
items/
Notes:
- Collection ids are case-sensitive and typically uppercase with
.separators. - The
metadata/items/folder is where generated Item JSON files are written.
Metadata Files
collection.json
How to Complete collection.json
Use metadata/collection.json as the authoritative STAC Collection metadata file for your dataset.
Required STAC Collection fields
The following fields must be completed and valid:
type— Must be set toCollection.stac_version— Use the current version (1.1.0).id— Your collection identifier. This must match:- your root folder name
collection_config.json > id
title— A human-readable name for your collection.description— A detailed description of the dataset.dedl:short_description— A concise version of the description. Displayed in the DestinE DataLake UI.license— The license under which the data is distributed.extent— Must include both spatial and temporal components.links— Must be present as an array.assets— Include if you have collection-level assets; otherwise use{}.
Recommended best practices
- Keep the
idconsistent across:- the collection root folder name
collection_config.json > id
- Use a clear
titleand a meaningfuldescriptionthat explains:- dataset content
- data source(s)
- intended use cases
- Ensure
extentis accurate so item-level metadata is interpreted correctly. - Only update necessary
links(typicallydescribedbyandcite-as). - Keep
typeandstac_versionunchanged unless upgrading STAC versions.
Field-specific guidance
links
- Always include at least:
license→ link to license termscite-as→ DOI or citation referencedescribedby→ external dataset documentation
- Ensure links are stable and publicly accessible.
- Use meaningful
titlevalues so users understand what each link represents.
extent
-
Spatial (
bbox):- Format:
[west, south, east, north] - Use WGS84 coordinates (EPSG:4326).
- Ensure the bounding box fully covers your dataset.
- Format:
-
Temporal (
interval):- Use ISO 8601 format (
YYYY-MM-DDTHH:MM:SSZ). - Use:
- a closed interval
[start, end]for fixed datasets [start, null]for ongoing datasets
- a closed interval
- Use ISO 8601 format (
license
- Prefer SPDX identifiers (e.g.
CC-BY-4.0,MIT,Apache-2.0). - Use:
proprietaryfor restricted datasetsvariousif multiple licenses apply
- If using
proprietaryorvarious:- explain clearly in
description - provide license links in
links
- explain clearly in
providers
- List all organizations involved in the dataset lifecycle.
- Common roles:
producer→ created the dataprocessor→ processed or transformed the datalicensor→ owns or distributes the data rightshost→ (optional) hosting platform
- Provide:
name(required)url(recommended)
- Include multiple providers if applicable rather than combining roles into one.
sci:publications
- Use this field to reference scientific publications or datasets.
- Include:
sci:doi→ DOI identifiersci:citation→ human-readable citation
- Add multiple entries if there are several related publications.
- Ensure DOIs resolve correctly (test the links).
keywords
- Provide relevant, searchable terms describing the dataset.
- Use:
- domain-specific terminology (e.g. Land Cover, Atmospheric Data)
- common synonyms where helpful
- Avoid overly generic terms (e.g. “data”, “analysis”).
assets
- Use for collection-level resources (not item-level data).
- Common examples:
thumbnail→ preview imageoverview→ documentation PDF
- Include:
href(URL)type(MIME type, e.g.image/png)roles(e.g.thumbnail,overview)
- If no assets are needed, use an empty object:
"assets": {}
collection_config.json
This controls how Item metadata generation behaves. Base example in EO.XXX.YYY.ZZZ/metadata/collection_config.json.
Configuration Options
The following options are user-facing and supported during item generation:
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | required | Collection id. Must match your collection root folder id. |
item_folder_level |
string | DD |
Where item folders are discovered. Allowed values: YYYY, MM, DD, NONE. |
item_config_optional |
boolean | false |
If false, each item folder is expected to contain item_config.json. |
item_asset_ignore_list |
list[string] | [] |
File names to ignore when creating STAC assets. |
media_type_overrides |
object | {} |
Optional extension to MIME type map used before default detection, e.g. { ".grib2": "application/x-grib2" }. |
role_overrides |
object | {} |
Optional MIME type to role map. Overrides default data/metadata classification. Allowed roles: data, metadata, thumbnail, overview. E.g. { "application/x-grib2": "data" }. File-name regex patterns (thumbnail_regex, overview_regex) still take priority. |
thumbnail_regex |
string | unset | Regex used to detect thumbnail assets and assign thumbnail role. |
overview_regex |
string | unset | Regex used to detect overview assets and assign overview role. |
additional_property_keys |
list[string] | [] |
Additional property suffix keys parsed from item folder names (standard mode). |
item_folder_naming_convention_type |
string | standard |
Naming convention mode. Allowed values: standard, non_standard_1. |
properties |
object | {} |
Additional properties merged into each generated Item. |
bbox |
list[number] | unset | Bounding box [minx, miny, maxx, maxy] applied to generated items. |
item_date_overide |
string | unset | Simplified mode override date in YYYYMMDD format. |
Important: the accepted key name is item_date_overide (spelling as shown).
Folder Granularity Options
Set item_folder_level in collection_config.json.
DD: item folders are expected underdata/YYYY/MM/DD/<item_folder>.MM: item folders are expected underdata/YYYY/MM/<item_folder>.YYYY: item folders are expected underdata/YYYY/<item_folder>.NONE: simplified mode. Any folder directly underdata/becomes an Item.
Standard Mode Example
{
"id": "EO.XXX.YYY.ZZZ",
"item_asset_ignore_list": ["item_config.json"],
"media_type_overrides": {
".grib2": "application/x-grib2"
},
"item_config_optional": false,
"item_folder_level": "DD",
"thumbnail_regex": "^thumbnail",
"overview_regex": "^overview",
"additional_property_keys": []
}
Simplified Mode Example
Simplified mode should only be used with agreement from support.
{
"id": "EO.XXX.YYY.ZZZ",
"item_asset_ignore_list": ["item_config.json"],
"item_folder_level": "NONE",
"item_date_overide": "20240101",
"thumbnail_regex": "^thumbnail",
"overview_regex": "^overview",
"additional_property_keys": [],
"bbox": [-10.0, 35.0, 10.0, 60.0]
}
Simplified mode datetime behavior:
- If
item_date_overideis set, that date is used for all generated Items. - Otherwise, if the first folder level under
data/is a year,YYYY-01-01is used. - Otherwise, the generation date is used.
Per-Item Configuration
Each item folder can include an item_config.json file. Typical per-item overrides:
bboxproperties- Asset-related regex fields when needed
When present, item-level values override collection-level values.
Generate Item Metadata
CLI Usage
Primary command (after installing the package):
generate-item-metadata EO.XXX.YYY.ZZZ
This default expects the collection at ./EO.XXX.YYY.ZZZ.
Optional collection subfolder path:
generate-item-metadata EO.XXX.YYY.ZZZ --collection-path toto
With this option, the collection is resolved at ./toto/EO.XXX.YYY.ZZZ.
For safety, --collection-path is currently restricted to subfolders under the current working directory.
Optional bucket override:
generate-item-metadata EO.XXX.YYY.ZZZ --bucket-name usergenerated-proposal-eo.xxx.yyy.zzz
Compatibility command (run from the project root):
python generate_item_metadata.py EO.XXX.YYY.ZZZ
Compatibility command with collection subfolder:
python generate_item_metadata.py EO.XXX.YYY.ZZZ --collection-path toto
Programmatic Usage
You can also use ItemGenerator directly in your Python scripts:
from usergenerated import ItemGenerator
# Create a generator for your collection
generator = ItemGenerator("EO.XXX.YYY.ZZZ")
# Generate Item metadata
generator.run()
# Resolve collection from ./toto/EO.XXX.YYY.ZZZ
generator_with_path = ItemGenerator("EO.XXX.YYY.ZZZ", collection_path="toto")
generator_with_path.run()
# With optional bucket name override
generator_with_bucket = ItemGenerator("EO.XXX.YYY.ZZZ", override_bucket_name="custom-bucket")
generator_with_bucket.run()
# Also write a reordered/normalised copy to output/collection_reordered.json (off by default)
generator_reordered = ItemGenerator("EO.XXX.YYY.ZZZ", save_reordered_collection=True)
generator_reordered.run()
Generated output is written to:
<COLLECTION_ID>/metadata/items/*.json
The ItemGenerator class is the primary API for programmatic use. The CLI command is a thin wrapper around this class.
Regenerate Item Metadata in S3
Use ItemGeneratorS3 to regenerate STAC Item metadata from bucket data while restricting updates to Item metadata only. This is useful when you need to update item generation logic or fix generated metadata without modifying data files in S3.
CLI Usage
Primary command (regenerate items in S3 for your collection):
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ
This command:
- Mirrors collection data from S3 to a local folder.
- Regenerates STAC Item metadata locally with your configured settings.
- Backs up existing S3 items by default.
- Replaces only the
metadata/itemsprefix in S3 (never modifies data files).
Optional dry-run (validate without modifying S3):
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --dry-run
Optional bucket name override:
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --bucket-name custom-bucket-name
Optional local mirror folder (default: tmp_structure_mirror):
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --local-root "./my_mirror"
Disable backup:
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --no-backup-items
Skip the ready_for_transfer safety check (see Safety Behavior; enabled by default):
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --no-use-ready-for-transfer-flag
Demonstration script usage (env-first configuration via .env/environment variables, see
script_s3_regenerate_item_metadata.py for the full list of S3_* variables):
python script_s3_regenerate_item_metadata.py
This script is intentionally a thin example of programmatic ItemGeneratorS3 usage.
For routine operations, prefer the regenerate-item-metadata-s3 console command.
The demo script additionally supports USE_READY_FOR_TRANSFER_FLAG (or the use_ready_for_transfer_flag
keyword argument to run_regenerate_item_metadata) to toggle the ready_for_transfer safety check described
below — see Safety Behavior. The primary console command exposes the same toggle via
--use-ready-for-transfer-flag/--no-use-ready-for-transfer-flag (shown above).
Programmatic Usage
You can also use ItemGeneratorS3 directly in your Python scripts:
from usergenerated import ItemGeneratorS3
# Create a generator for your collection
generator = ItemGeneratorS3(
"EO.XXX.YYY.ZZZ",
dry_run=True, # Validate without modifying S3
)
# Regenerate Item metadata
generator.run()
# With explicit bucket name
generator_with_bucket = ItemGeneratorS3(
"EO.XXX.YYY.ZZZ",
bucket_name="custom-bucket",
backup_items=True,
)
generator_with_bucket.run()
# Skip the ready_for_transfer safety check (see Safety Behavior below); enabled by default
generator_without_gate = ItemGeneratorS3(
"EO.XXX.YYY.ZZZ",
use_ready_for_transfer_flag=False,
)
generator_without_gate.run()
Safety Behavior
The regeneration workflow includes several safety measures:
- Validates credentials and bucket structure before processing.
- Mirrors S3 content locally and ensures local processing under
<collection_id>/. - By default (
use_ready_for_transfer_flag=True), requires"ready_for_transfer": truein the mirrored collection'smetadata/collection_config.json; aborts with an error if the field is missing orfalse(this check applies even with--dry-run). Skip this check via--no-use-ready-for-transfer-flagon the CLI, theuse_ready_for_transfer_flag=Falseconstructor argument, orUSE_READY_FOR_TRANSFER_FLAG=falsefor the demo script. - Regenerates Item metadata locally with existing generation logic.
- Backs up existing
<collection_id>/metadata/itemsby default (can be disabled). - Replaces only
<collection_id>/metadata/itemsin S3 (never modifiesdata/). - With
--dry-run, performs local validation and regeneration, then prints planned S3 actions without modifying the bucket.
Mirroring Behavior
The mirroring process downloads real files selectively to improve performance:
- Downloads real files only for
metadata/*anditem_config.jsonkeys. - Writes per-folder manifest files (
usergenerated_s3_folder_manifest.jsonl) for discovered objects. - Uses manifest entries during item generation so full non-metadata assets are not required locally.
- Supports optional placeholder file creation for non-fetched objects (disabled in the S3 regeneration workflow).
- Logs a summary with discovered, downloaded, and failed object counts, plus periodic progress logs during large scans.
Maintainer Notes
Recent internal refactoring improved testability and packaging workflows.
- The primary command is now
generate-item-metadata <COLLECTION_ID>. - generate_item_metadata.py is retained as a compatibility wrapper.
- Internal logic was modularized into reusable helpers under
usergenerated/for:
- collection path resolution
- item folder discovery by configured granularity
- item config precedence/loading
- simplified and hierarchy-based datetime resolution
- This split enables unit-heavy testing of pure logic while keeping entry-point orchestration thin.
Optional S3 Upload
Automatic upload is configured at runtime via ItemGenerator constructor parameters
or CLI flags (not via module constants).
Programmatic example:
from usergenerated import ItemGenerator
generator = ItemGenerator(
"EO.XXX.YYY.ZZZ",
is_upload_s3=True,
s3_endpoint_url="https://s3.central.data.destination-earth.eu",
s3_user_generated_bucket_prefix="usergenerated-proposal",
)
generator.run()
CLI example:
generate-item-metadata EO.XXX.YYY.ZZZ \
--upload-s3 \
--s3-endpoint-url "https://s3.central.data.destination-earth.eu" \
--s3-bucket-prefix "usergenerated-proposal"
If upload is enabled, create a .env file at the project root with credentials:
AWS_ACCESS_KEY_ID="<your_access_key_id>"
AWS_SECRET_ACCESS_KEY="<your_secret_access_key>"
For manual upload workflows, see README-s3cmd.md.
S3 utility wrappers are available for programmatic execution around S3Tools:
from script_s3_create_bucket import run_create_bucket
from script_s3_move_bucket_contents import run_move_bucket_contents
from script_s3_upload_file_to_bucket import run_upload_file
from script_s3_upload_folder_to_bucket import run_upload_folder
run_create_bucket(
aws_access_key_id="<key>",
aws_secret_access_key="<secret>",
s3_bucket_name="my-bucket",
)
run_upload_file(
file_name="EO.XXX.YYY.ZZZ/metadata/collection.json",
object_name="EO.XXX.YYY.ZZZ/metadata/collection.json",
aws_access_key_id="<key>",
aws_secret_access_key="<secret>",
s3_bucket_name="my-bucket",
)
run_upload_folder(
folder_path="EO.XXX.YYY.ZZZ/metadata/items",
target_path="EO.XXX.YYY.ZZZ/metadata/items",
aws_access_key_id="<key>",
aws_secret_access_key="<secret>",
s3_bucket_name="my-bucket",
)
run_move_bucket_contents(
aws_access_key_id="<key>",
aws_secret_access_key="<secret>",
s3_bucket_name="my-bucket",
s3_target_prefix="archive",
)
S3 Folder Upload Progress
When using run_upload_folder(...) or S3Tools.upload_folder_to_s3(...), uploads now display progress bars:
- A global
Total filesbar showing overall folder upload progress. - Per-file bars (bytes transferred) for active uploads.
Current behavior:
- Uploads run concurrently with a bounded worker pool.
- Per-file progress bars are capped to a visible range and positions are reused for large uploads.
usergenerated_s3_folder_manifest.jsonlfiles and symlinks are skipped from upload discovery.- A final summary logs discovered, uploaded, and failed file counts.
These wrappers accept explicit parameters first and only fall back to environment variables when a parameter is omitted.
Regenerate Item Metadata: Use the CLI command for routine operations (see Regenerate Item Metadata in S3):
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --dry-run
The script_s3_regenerate_item_metadata.py file is kept as a small demonstrator of ItemGeneratorS3 programmatic usage. Use the CLI command or the ItemGeneratorS3 class directly for real workflows.
Copy a Collection Between Buckets
script_s3_copy_source_bucket_collection_to_target_bucket_collection.py copies a collection
folder from a source S3 bucket/account to a target S3 bucket/account, using
usergenerated.s3.s3_collection_copier.CollectionCopier. It:
- Detects the source bucket's
BucketLayout(COLLECTION_ROOTorFLAT_ROOT) and normalizes the target to always be<collection_id>/...(COLLECTION_ROOT), fixing upFLAT_ROOTsources in the process. - Never modifies the source bucket.
- Is safely re-runnable: objects already present in the target (by key) are skipped, so an interrupted transfer can just be re-run to continue where it left off.
- Streams each object directly from source to target (no local disk buffering), since source and target buckets are assumed to use different credentials/accounts.
Configure both buckets in .env (the source variables are the same ones used elsewhere in this
project; target variables are TARGET_-prefixed):
# Source bucket (existing variables, unchanged)
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
S3_BUCKET_NAME="my-source-bucket"
AWS_ACCESS_KEY_ID="<source_access_key_id>"
AWS_SECRET_ACCESS_KEY="<source_secret_access_key>"
# Target bucket
TARGET_S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
TARGET_S3_BUCKET_NAME="my-target-bucket"
TARGET_AWS_ACCESS_KEY_ID="<target_access_key_id>"
TARGET_AWS_SECRET_ACCESS_KEY="<target_secret_access_key>"
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
S3_FRESH_START="false" # set true to delete the target's existing '<collection_id>/' prefix first
S3_DRY_RUN="false" # set true to preview the copy plan without modifying the target
Then run:
python script_s3_copy_source_bucket_collection_to_target_bucket_collection.py
S3_FRESH_START is the only way to trigger the destructive "clean the target path first" step
(e.g. for a first run against a target that already has stray/partial contents); it is never
triggered automatically. Omit it (or leave it false) for the default incremental,
resume-friendly sync.
Folder uploads use object keys relative to the selected local folder. For example,
uploading ./EO.XXX.YYY.ZZZ/metadata/items with no target_path uploads files like
a.json and nested/b.json at the bucket root. To place files under a collection or
metadata prefix in the bucket, pass that prefix explicitly as target_path.
Verify a Collection Copy
script_s3_verify_collection_copy.py is a read-only smoke test to run right after the copy
script above completes: it uses usergenerated.s3.s3_collection_copy_verifier.CollectionCopyVerifier
to list the collection's objects in both buckets and confirms the target has exactly the same
relative keys as the source, with matching sizes for every key. Neither bucket is modified.
It reuses the same .env variables as the copy script above (no new variables needed):
python script_s3_verify_collection_copy.py
The script exits 0 and prints Collection copy verification passed: True when the target
matches the source. On a mismatch CollectionCopyVerifier.run() raises S3OperationError
whose message summarises the missing, unexpected, and/or size-mismatched keys (sampled, up to
20 per category); the script reports that one line and exits 1.
Similarly, CollectionCopier.run() (the copy script above) raises S3OperationError — naming
the failed-object count and a sample of failed keys — if any object fails to copy. Objects
that did copy are left in place, so re-running resumes from where it stopped.
S3 helper code lives under usergenerated/s3/.
Promote a Collection (regenerate → copy → verify → read, in one call)
usergenerated.s3.collection_promoter.promote_collection (also
from usergenerated import promote_collection) runs the whole "a collection has been staged
in a source bucket; validate it, copy it to a target bucket, and hand me its items" workflow
as a single call, in strict order:
ItemGeneratorS3(...).run()— regenerate STAC item metadata in the source bucket (source credentials; theready_for_transfersafety gate stays on;data/is untouched).CollectionCopier(...).run()— copy the collection source bucket → target bucket.CollectionCopyVerifier(...).run()— confirm the target matches the source key-for-key.- Build a
PromotedCollectionread accessor over the target copy and callon_ready(handle)exactly once.
on_ready is a callback you provide. Its argument exposes just two methods:
handle.read_collection() -> dict— the target copy'scollection.json(wrapsget_collection_from_s3).handle.iter_items() -> Iterator[dict]— the target copy's STAC Items, streamed one at a time (wrapsiter_collection_items_from_s3; lazy — consume it inside the callback).
Any domain-specific downstream work (database load, re-publish, indexing) stays entirely in
your on_ready; promote_collection owns no datastore and never returns a status — a normal
return means every stage passed and on_ready ran.
on_readyruns only after copy and verify have both succeeded. Any earlier failure raises (S3OperationError/ConfigError/CredentialsError/FileNotFoundError, andFileExistsErrorfrom regeneration) andon_readyis never called.dry_run=Trueruns regeneration/validation only (viaItemGeneratorS3(dry_run=True)); the copier and verifier are never constructed andon_readyis never called.fresh_start=Truepasses through toCollectionCopier, deleting the target<collection_id>/prefix before copying. The source bucket is never modified.- Idempotent / resumable: re-running after a failure repeats the skip-aware copy, re-verifies,
and re-invokes
on_ready. Side effects are only on the source (item regen, plus a timestampeditemsbackup whenbackup_items=True) and the target (the copy).
from usergenerated import PromotedCollection, promote_collection
def on_ready(collection: PromotedCollection) -> None:
stac = collection.read_collection()
item_count = sum(1 for _ in collection.iter_items())
print(f"{stac['id']}: {item_count} item(s) ready in the target bucket")
promote_collection(
collection_id="EO.XXX.YYY.ZZZ",
source_endpoint_url="https://s3.central.data.destination-earth.eu",
source_bucket_name="my-source-bucket",
source_aws_access_key_id="<source_access_key_id>",
source_aws_secret_access_key="<source_secret_access_key>",
target_endpoint_url="https://s3.central.data.destination-earth.eu",
target_bucket_name="my-target-bucket",
target_aws_access_key_id="<target_access_key_id>",
target_aws_secret_access_key="<target_secret_access_key>",
on_ready=on_ready,
)
script_s3_promote_collection.py is a runnable .env-driven demo whose on_ready logs the
promoted collection's id and item count. It reuses the same variables as the copy and
regenerate scripts (S3_COLLECTION_ID, S3_ENDPOINT_URL, S3_BUCKET_NAME,
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, TARGET_S3_ENDPOINT_URL,
TARGET_S3_BUCKET_NAME, TARGET_AWS_ACCESS_KEY_ID, TARGET_AWS_SECRET_ACCESS_KEY,
S3_DRY_RUN, S3_FRESH_START), plus two optional keys read only by this script:
S3_LOCAL_ROOT (default tmp_structure_mirror) and S3_BACKUP_ITEMS (default true).
python script_s3_promote_collection.py
Manipulate Collection Config in S3
script_s3_collection_config_manipulation.py demonstrates the helpers in
usergenerated.s3.s3_collection_config_tools for reading and updating a collection's
collection_config.json directly in S3, without a local mirror. Unlike the verify script above, it
mutates the live bucket. For a given collection it:
- Resolves the config key with
resolve_collection_config_keyand prints the fullcollection_config.json. - Reads the
ready_for_transferflag viaget_ready_for_transfer. - Sets
ready_for_transfertofalseviaset_ready_for_transfer. - Adds an ad-hoc
last_transferredfield (current UTC time, ISO 8601) viaupdate_collection_config.
Setting ready_for_transfer to false engages the regenerate-item-metadata-s3 safety gate described
in Safety Behavior — a subsequent regeneration run will refuse to proceed until the
flag is set back to true (or the check is explicitly skipped).
For programmatic use the same module exposes two reusable building blocks so a caller does not have to chain layout detection and a hand-written read-modify-write:
resolve_collection_config_key(s3_client, bucket_name, collection_id)— locatecollection_config.jsonin the bucket without handlingBucketLayoutyourself.update_collection_config(s3_client, bucket_name, key, changes)— one read-modify-write for an arbitrary set of fields.changesis either a mapping merged into the config (use the exportedREMOVEsentinel as a value to delete a key) or a callback that mutates the dict in place. Keys you do not touch keep their value, order, and the file'sindent=4formatting; it raisesConfigErrorif the object is missing or not a JSON object andS3OperationErroron an S3 read/write failure.set_ready_for_transferis a one-line wrapper over it.
Configure the target bucket in .env (the same single-bucket variables used by the regeneration demo
script):
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
S3_BUCKET_NAME="my-bucket"
AWS_ACCESS_KEY_ID="<access_key_id>"
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
Then run:
python script_s3_collection_config_manipulation.py
Report Bucket Information
script_s3_bucket_information.py demonstrates usergenerated.s3.s3_bucket_info.get_bucket_information,
which makes a single paginated sweep of a bucket (or a prefix within it) and prints a JSON report:
file count, total and average size, the 5 largest files, oldest/most-recent modification times, a
count+size breakdown by top-level folder, a generated_at timestamp, and a sample file listing
capped at 50 entries (with a sample_truncated flag) so you never get thousands of rows back.
Every size in the report (total_size_bytes, average_file_size_bytes, and each size_bytes in
largest_files, top_level_prefixes, and sample_files) has a ..._human/size_human
counterpart, e.g. "total_size_human": "94.2 MB". It is read-only — the bucket is never
modified.
Writing the report to output/<YYYYMMDDTHHMMSS>_Bucket_Information.json (the timestamp matches
generated_at in the JSON) is off by default — the report is only printed to stdout. Turn it
on with either S3_WRITE_OUTPUT_FILE="true" in .env, or write_output_file=True when calling
run_get_bucket_information programmatically; the output/ directory is created if needed.
Configure the bucket in .env (the same single-bucket variables used by the config manipulation
script above); S3_PREFIX, S3_BUCKET_INFO_SAMPLE_LIMIT, S3_WRITE_OUTPUT_FILE, and
S3_BUCKET_INFO_OUTPUT_DIR are optional:
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
S3_BUCKET_NAME="my-bucket"
AWS_ACCESS_KEY_ID="<access_key_id>"
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
S3_PREFIX="" # optional: scope the report to one folder/collection
S3_BUCKET_INFO_SAMPLE_LIMIT="50" # optional: cap on the sample file listing
S3_WRITE_OUTPUT_FILE="false" # optional: set true/1/yes/on to also write the JSON report to disk
S3_BUCKET_INFO_OUTPUT_DIR="output" # optional: where the timestamped JSON report is written
Then run:
python script_s3_bucket_information.py
Copy Collection Metadata Locally
script_s3_get_metadata.py demonstrates
usergenerated.s3.s3_collection_metadata_downloader.download_collection_metadata, which copies a
collection's metadata/ folder from S3 to a local destination. The source bucket's layout
(whether it holds <collection_id>/metadata/ or a top-level metadata/) is detected
automatically, so the caller never needs to know or check it beforehand — the result always lands
under a folder named for the collection ID, e.g. <destination_root>/EO.XXX.YYY.ZZZ/metadata. It
is read-only with respect to S3 — nothing in the source bucket is modified.
The destination root is configurable via S3_METADATA_DESTINATION_ROOT (or the
destination_root argument when calling run_get_metadata programmatically); when unset, it
defaults to a fresh timestamped folder under /tmp/usergenerated_tmp/, e.g.
/tmp/usergenerated_tmp/20260824T101500/EO.XXX.YYY.ZZZ/metadata.
Objects are downloaded concurrently — one worker thread per object, bounded by the
max_workers argument (default MAX_METADATA_DOWNLOAD_WORKERS = 16, capped at 32). The single
s3_client is shared across those threads, so build it with a matching connection pool:
create_s3_client(..., max_pool_connections=MAX_METADATA_DOWNLOAD_WORKERS) (botocore defaults
to 10). If any object fails to download the whole call raises S3OperationError (a
RuntimeError subclass) rather than returning a partial folder; once a systemic problem has
failed ~20 objects it stops submitting
new downloads instead of grinding through every remaining key.
For callers that only need the files for the duration of a block — parse collection.json,
stream items/ somewhere, done — use the downloaded_collection_metadata context manager
instead of calling download_collection_metadata directly; on exit it removes the
<collection_id>/ subtree it downloaded, and if it also generated the timestamped destination
root (no explicit destination_root given) it removes that too, so nothing is left under
/tmp. An explicit destination_root you pass in is otherwise left in place:
with downloaded_collection_metadata(s3_client, bucket, collection_id) as metadata_folder:
collection = get_stac_collection(metadata_folder)
for item in iter_items(metadata_folder):
...
# the timestamped /tmp/usergenerated_tmp/<...> directory is gone here
The same module also provides small supporting functions for reading the downloaded metadata:
get_stac_collection/get_collection_config load collection.json/collection_config.json as
dicts, and iter_items/iter_item_batches stream STAC Items out of the downloaded items/
folder — one item at a time, or in fixed-size batches — rather than loading every item into
memory at once. This matters for collections with very large item counts (hundreds of thousands),
where materializing every item as a single list would be memory-intensive. The batch size defaults
to METADATA_DOWNLOADER_DEFAULT_ITEM_BATCH_SIZE (500) and is configurable via
S3_METADATA_ITEM_BATCH_SIZE (or the item_batch_size argument to run_get_metadata).
If you only need to walk the items once and don't need them to persist,
iter_collection_items_from_s3 / iter_collection_item_batches_from_s3 (and
get_collection_from_s3 for collection.json) stream STAC Items straight from the bucket with no
local copy written and no cleanup to do — a drop-in for iter_items / iter_item_batches /
get_stac_collection. The bucket layout is detected automatically, item bodies are fetched with a
bounded concurrent prefetch (max_workers defaults to MAX_METADATA_DOWNLOAD_WORKERS, capped at
32), and any fetch or parse failure raises S3OperationError rather than yielding a partial
result. Keep download_collection_metadata for when the files must stay on disk or be read more
than once. Setting S3_METADATA_STREAM_ITEMS to a truthy value (or passing stream_items=True to
run_get_metadata) runs script_s3_get_metadata.py on this no-download path.
Configure the collection and bucket in .env (the same single-bucket variables used by the config
manipulation script above); S3_METADATA_DESTINATION_ROOT, S3_METADATA_ITEM_BATCH_SIZE, and
S3_METADATA_STREAM_ITEMS are optional:
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
S3_BUCKET_NAME="my-bucket"
AWS_ACCESS_KEY_ID="<access_key_id>"
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
S3_METADATA_DESTINATION_ROOT="" # optional: local folder to copy metadata/ into (default: /tmp/usergenerated_tmp/<timestamp>)
S3_METADATA_ITEM_BATCH_SIZE="" # optional: items per batch when streaming items/ (default: 500)
S3_METADATA_STREAM_ITEMS="" # optional: truthy = skip the local download, stream items straight from S3
Then run:
python script_s3_get_metadata.py
Runtime Initialization
Logging initialization is now explicit and idempotent.
- Entry-point scripts call
setup_logging()fromusergenerated.logging_configat startup. - Environment loading (
load_dotenv) is also called explicitly at startup. - Importing modules should not configure logging implicitly.
This keeps behavior stable regardless of import order and avoids duplicate log handlers.
Exit codes and failure output
Every console command (generate-item-metadata, regenerate-item-metadata-s3) and root
script_s3_*.py shares one convention:
| Exit code | Meaning |
|---|---|
0 |
success |
1 |
an expected failure (bad config, missing credentials, a missing file or folder you named, invalid collection.json, a failed S3 operation) — reported as a single ERROR log line, no traceback; or an unexpected internal error, which keeps its traceback |
2 |
command-line usage error (bad or missing arguments) |
Troubleshooting
-
Generation fails with folder-level errors: Set
item_folder_levelto match your actualdata/layout. -
Missing
item_config.jsonerrors: Setitem_config_optionaltotrueif item config files are intentionally omitted. -
Collection id validation errors: Ensure root folder name and
collection_config.json > idrepresent the same collection id. -
Upload credential errors: Ensure
.envcontains validAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYwhen upload is enabled.
External References
Metadata
Release files for usergenerated 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| usergenerated-2.0.0.tar.gz | 124.8 kB | Without provenance | Details for usergenerated-2.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| usergenerated-2.0.0-py3-none-any.whl | Python 3 | none | any | Details for usergenerated-2.0.0-py3-none-any.whl |
Total release size: 202.6 kB
Release files / usergenerated-2.0.0.tar.gz
| Download URL | usergenerated-2.0.0.tar.gz |
|---|---|
| Size | 124.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
93c24e50b159c0059dd144e3a10d70aea18c2665e03a8d0a01c5c1bef27ed49e
|
|
BLAKE2b-256 checksum How to use checksums |
65edd2d7d1b42941e260c77ad9bc7ad8a20fbf5c8221dc99634b4db2ae068349
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / usergenerated-2.0.0-py3-none-any.whl
| Download URL | usergenerated-2.0.0-py3-none-any.whl |
|---|---|
| Size | 77.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f3aac79a592ab237b6e4bc319eb565ddc93dc23a3d92e9702e469d13bfd5b40e
|
|
BLAKE2b-256 checksum How to use checksums |
76a21a51acf756065c1bfb95c2bcf560f396130a2ba0025810b8b242436d715d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for usergenerated 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.