usergenerated 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- usergenerated-2.0.0/PKG-INFO +951 -0
- usergenerated-2.0.0/README.md +925 -0
- usergenerated-2.0.0/generate_item_metadata.py +167 -0
- usergenerated-2.0.0/pyproject.toml +70 -0
- usergenerated-2.0.0/setup.cfg +4 -0
- usergenerated-2.0.0/tests/test_cli.py +214 -0
- usergenerated-2.0.0/tests/test_cli_common.py +48 -0
- usergenerated-2.0.0/tests/test_collection_paths.py +64 -0
- usergenerated-2.0.0/tests/test_collection_promoter.py +445 -0
- usergenerated-2.0.0/tests/test_confighelper.py +61 -0
- usergenerated-2.0.0/tests/test_confighelper_stac_io.py +30 -0
- usergenerated-2.0.0/tests/test_datetools.py +17 -0
- usergenerated-2.0.0/tests/test_env_utils.py +41 -0
- usergenerated-2.0.0/tests/test_errors.py +43 -0
- usergenerated-2.0.0/tests/test_generate_item_metadata_integration.py +370 -0
- usergenerated-2.0.0/tests/test_item_config_resolver.py +71 -0
- usergenerated-2.0.0/tests/test_item_datetime_resolution.py +85 -0
- usergenerated-2.0.0/tests/test_item_folder_discovery.py +65 -0
- usergenerated-2.0.0/tests/test_item_generator_get_item.py +159 -0
- usergenerated-2.0.0/tests/test_item_generator_run.py +205 -0
- usergenerated-2.0.0/tests/test_itemhelper_media_type.py +186 -0
- usergenerated-2.0.0/tests/test_logging_config.py +67 -0
- usergenerated-2.0.0/tests/test_s3_bucket_info.py +184 -0
- usergenerated-2.0.0/tests/test_s3_cli.py +166 -0
- usergenerated-2.0.0/tests/test_s3_collection_config_tools.py +324 -0
- usergenerated-2.0.0/tests/test_s3_collection_copier.py +289 -0
- usergenerated-2.0.0/tests/test_s3_collection_copy_verifier.py +178 -0
- usergenerated-2.0.0/tests/test_s3_collection_metadata_downloader.py +598 -0
- usergenerated-2.0.0/tests/test_s3_entrypoint.py +18 -0
- usergenerated-2.0.0/tests/test_s3_item_generator.py +765 -0
- usergenerated-2.0.0/tests/test_s3_mirror.py +99 -0
- usergenerated-2.0.0/tests/test_s3_mirror_should_fetch_real_file.py +18 -0
- usergenerated-2.0.0/tests/test_s3tools_upload_folder_target_path.py +521 -0
- usergenerated-2.0.0/tests/test_script_s3_bucket_information.py +267 -0
- usergenerated-2.0.0/tests/test_script_s3_collection_config_manipulation.py +160 -0
- usergenerated-2.0.0/tests/test_script_s3_copy_source_bucket_collection_to_target_bucket_collection.py +178 -0
- usergenerated-2.0.0/tests/test_script_s3_get_metadata.py +220 -0
- usergenerated-2.0.0/tests/test_script_s3_promote_collection.py +205 -0
- usergenerated-2.0.0/tests/test_script_s3_regenerate_item_metadata.py +168 -0
- usergenerated-2.0.0/tests/test_script_s3_utilities.py +396 -0
- usergenerated-2.0.0/tests/test_script_s3_verify_collection_copy.py +170 -0
- usergenerated-2.0.0/usergenerated/__init__.py +24 -0
- usergenerated-2.0.0/usergenerated/cli.py +80 -0
- usergenerated-2.0.0/usergenerated/cli_common.py +35 -0
- usergenerated-2.0.0/usergenerated/config/__init__.py +0 -0
- usergenerated-2.0.0/usergenerated/config/collection_paths.py +102 -0
- usergenerated-2.0.0/usergenerated/config/confighelper.py +362 -0
- usergenerated-2.0.0/usergenerated/config/constants.py +71 -0
- usergenerated-2.0.0/usergenerated/config/item_config_resolver.py +38 -0
- usergenerated-2.0.0/usergenerated/datetools.py +15 -0
- usergenerated-2.0.0/usergenerated/env_utils.py +43 -0
- usergenerated-2.0.0/usergenerated/errors.py +43 -0
- usergenerated-2.0.0/usergenerated/item/__init__.py +0 -0
- usergenerated-2.0.0/usergenerated/item/folder_discovery.py +44 -0
- usergenerated-2.0.0/usergenerated/item/itemhelper.py +365 -0
- usergenerated-2.0.0/usergenerated/item/path_datetime.py +79 -0
- usergenerated-2.0.0/usergenerated/item_generator.py +600 -0
- usergenerated-2.0.0/usergenerated/logging_config.py +75 -0
- usergenerated-2.0.0/usergenerated/s3/__init__.py +1 -0
- usergenerated-2.0.0/usergenerated/s3/collection_promoter.py +214 -0
- usergenerated-2.0.0/usergenerated/s3/s3_bucket_info.py +148 -0
- usergenerated-2.0.0/usergenerated/s3/s3_collection_config_tools.py +235 -0
- usergenerated-2.0.0/usergenerated/s3/s3_collection_copier.py +289 -0
- usergenerated-2.0.0/usergenerated/s3/s3_collection_copy_verifier.py +185 -0
- usergenerated-2.0.0/usergenerated/s3/s3_collection_metadata_downloader.py +495 -0
- usergenerated-2.0.0/usergenerated/s3/s3_mirror.py +404 -0
- usergenerated-2.0.0/usergenerated/s3/s3tools.py +570 -0
- usergenerated-2.0.0/usergenerated/s3_cli.py +50 -0
- usergenerated-2.0.0/usergenerated/s3_cli_common.py +66 -0
- usergenerated-2.0.0/usergenerated/s3_item_generator.py +370 -0
- usergenerated-2.0.0/usergenerated.egg-info/PKG-INFO +951 -0
- usergenerated-2.0.0/usergenerated.egg-info/SOURCES.txt +74 -0
- usergenerated-2.0.0/usergenerated.egg-info/dependency_links.txt +1 -0
- usergenerated-2.0.0/usergenerated.egg-info/entry_points.txt +3 -0
- usergenerated-2.0.0/usergenerated.egg-info/requires.txt +21 -0
- usergenerated-2.0.0/usergenerated.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usergenerated
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Utilities to prepare User Generated collections for DestinE Data Lake STAC workflows.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pystac[orjson,validation]
|
|
8
|
+
Requires-Dist: shapely
|
|
9
|
+
Requires-Dist: python-magic
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: beautifulsoup4
|
|
12
|
+
Requires-Dist: netCDF4
|
|
13
|
+
Requires-Dist: boto3
|
|
14
|
+
Requires-Dist: botocore
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy; extra == "dev"
|
|
21
|
+
Requires-Dist: pip-tools; extra == "dev"
|
|
22
|
+
Requires-Dist: twine; extra == "dev"
|
|
23
|
+
Provides-Extra: collection-tests
|
|
24
|
+
Requires-Dist: tqdm; extra == "collection-tests"
|
|
25
|
+
Requires-Dist: destinelab; extra == "collection-tests"
|
|
26
|
+
|
|
27
|
+
# User Generated Data Process
|
|
28
|
+
|
|
29
|
+
Table of Contents:
|
|
30
|
+
1. [Overview](#overview)
|
|
31
|
+
2. [User Workflow](#user-workflow)
|
|
32
|
+
3. [Required Structure](#required-structure)
|
|
33
|
+
4. [Metadata Files](#metadata-files)
|
|
34
|
+
- [collection.json](#collectionjson)
|
|
35
|
+
- [collection_config.json](#collection_configjson)
|
|
36
|
+
5. [Generate Item Metadata](#generate-item-metadata)
|
|
37
|
+
6. [Regenerate Item Metadata in S3](#regenerate-item-metadata-in-s3)
|
|
38
|
+
7. [Maintainer Notes](#maintainer-notes)
|
|
39
|
+
8. [Optional S3 Upload](#optional-s3-upload)
|
|
40
|
+
9. [Runtime Initialization](#runtime-initialization)
|
|
41
|
+
10. [Troubleshooting](#troubleshooting)
|
|
42
|
+
11. [External References](#external-references)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
This project helps you prepare a User Generated collection for DestinE Data Lake review.
|
|
47
|
+
|
|
48
|
+
Use this guide to:
|
|
49
|
+
1. Structure your collection folders and data assets.
|
|
50
|
+
2. Configure metadata files.
|
|
51
|
+
3. Generate STAC Item metadata.
|
|
52
|
+
|
|
53
|
+
For contributor and maintenance documentation, see [docs/development.md](docs/development.md).
|
|
54
|
+
|
|
55
|
+
## User Workflow
|
|
56
|
+
|
|
57
|
+
1. Create your collection root folder using the provided collection id, for example `EO.XXX.YYY.ZZZ`.
|
|
58
|
+
2. If needed, place the collection in a subfolder under your current working directory and pass that subfolder with `--collection-path`.
|
|
59
|
+
3. Add metadata files under `metadata/`.
|
|
60
|
+
4. Organize data assets under `data/` according to your configured folder level.
|
|
61
|
+
5. Run [generate_item_metadata.py](generate_item_metadata.py) to validate and generate STAC Items under `metadata/items/`.
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
Reference collection structure is available in [EO.XXX.YYY.ZZZ](EO.XXX.YYY.ZZZ).
|
|
66
|
+
|
|
67
|
+
## Required Structure
|
|
68
|
+
|
|
69
|
+
Expected top-level structure:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
EO.XXX.YYY.ZZZ/
|
|
73
|
+
data/
|
|
74
|
+
metadata/
|
|
75
|
+
collection.json
|
|
76
|
+
collection_config.json
|
|
77
|
+
items/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
1. Collection ids are case-sensitive and typically uppercase with `.` separators.
|
|
82
|
+
2. The `metadata/items/` folder is where generated Item JSON files are written.
|
|
83
|
+
|
|
84
|
+
## Metadata Files
|
|
85
|
+
|
|
86
|
+
### collection.json
|
|
87
|
+
|
|
88
|
+
#### How to Complete `collection.json`
|
|
89
|
+
|
|
90
|
+
Use `metadata/collection.json` as the authoritative STAC Collection metadata file for your dataset.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
#### Required STAC Collection fields
|
|
95
|
+
|
|
96
|
+
The following fields **must** be completed and valid:
|
|
97
|
+
|
|
98
|
+
1. `type` — Must be set to `Collection`.
|
|
99
|
+
2. `stac_version` — Use the current version (`1.1.0`).
|
|
100
|
+
3. `id` — Your collection identifier. This must match:
|
|
101
|
+
- your root folder name
|
|
102
|
+
- `collection_config.json > id`
|
|
103
|
+
4. `title` — A human-readable name for your collection.
|
|
104
|
+
5. `description` — A detailed description of the dataset.
|
|
105
|
+
6. `dedl:short_description` — A concise version of the description. Displayed in the DestinE DataLake UI.
|
|
106
|
+
7. `license` — The license under which the data is distributed.
|
|
107
|
+
8. `extent` — Must include both spatial and temporal components.
|
|
108
|
+
9. `links` — Must be present as an array.
|
|
109
|
+
10. `assets` — Include if you have collection-level assets; otherwise use `{}`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
#### Recommended best practices
|
|
114
|
+
|
|
115
|
+
- Keep the `id` consistent across:
|
|
116
|
+
- the collection root folder name
|
|
117
|
+
- `collection_config.json > id`
|
|
118
|
+
- Use a clear `title` and a meaningful `description` that explains:
|
|
119
|
+
- dataset content
|
|
120
|
+
- data source(s)
|
|
121
|
+
- intended use cases
|
|
122
|
+
- Ensure `extent` is accurate so item-level metadata is interpreted correctly.
|
|
123
|
+
- Only update necessary `links` (typically `describedby` and `cite-as`).
|
|
124
|
+
- Keep `type` and `stac_version` unchanged unless upgrading STAC versions.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
#### Field-specific guidance
|
|
129
|
+
|
|
130
|
+
##### `links`
|
|
131
|
+
- Always include at least:
|
|
132
|
+
- `license` → link to license terms
|
|
133
|
+
- `cite-as` → DOI or citation reference
|
|
134
|
+
- `describedby` → external dataset documentation
|
|
135
|
+
- Ensure links are stable and publicly accessible.
|
|
136
|
+
- Use meaningful `title` values so users understand what each link represents.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
##### `extent`
|
|
141
|
+
- **Spatial (`bbox`)**:
|
|
142
|
+
- Format: `[west, south, east, north]`
|
|
143
|
+
- Use WGS84 coordinates (EPSG:4326).
|
|
144
|
+
- Ensure the bounding box fully covers your dataset.
|
|
145
|
+
|
|
146
|
+
- **Temporal (`interval`)**:
|
|
147
|
+
- Use ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`).
|
|
148
|
+
- Use:
|
|
149
|
+
- a closed interval `[start, end]` for fixed datasets
|
|
150
|
+
- `[start, null]` for ongoing datasets
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
##### `license`
|
|
155
|
+
- Prefer SPDX identifiers (e.g. `CC-BY-4.0`, `MIT`, `Apache-2.0`).
|
|
156
|
+
- Use:
|
|
157
|
+
- `proprietary` for restricted datasets
|
|
158
|
+
- `various` if multiple licenses apply
|
|
159
|
+
- If using `proprietary` or `various`:
|
|
160
|
+
- explain clearly in `description`
|
|
161
|
+
- provide license links in `links`
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
##### `providers`
|
|
166
|
+
- List all organizations involved in the dataset lifecycle.
|
|
167
|
+
- Common roles:
|
|
168
|
+
- `producer` → created the data
|
|
169
|
+
- `processor` → processed or transformed the data
|
|
170
|
+
- `licensor` → owns or distributes the data rights
|
|
171
|
+
- `host` → (optional) hosting platform
|
|
172
|
+
- Provide:
|
|
173
|
+
- `name` (required)
|
|
174
|
+
- `url` (recommended)
|
|
175
|
+
- Include multiple providers if applicable rather than combining roles into one.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
##### `sci:publications`
|
|
180
|
+
- Use this field to reference scientific publications or datasets.
|
|
181
|
+
- Include:
|
|
182
|
+
- `sci:doi` → DOI identifier
|
|
183
|
+
- `sci:citation` → human-readable citation
|
|
184
|
+
- Add multiple entries if there are several related publications.
|
|
185
|
+
- Ensure DOIs resolve correctly (test the links).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
##### `keywords`
|
|
190
|
+
- Provide relevant, searchable terms describing the dataset.
|
|
191
|
+
- Use:
|
|
192
|
+
- domain-specific terminology (e.g. *Land Cover*, *Atmospheric Data*)
|
|
193
|
+
- common synonyms where helpful
|
|
194
|
+
- Avoid overly generic terms (e.g. “data”, “analysis”).
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
##### `assets`
|
|
199
|
+
- Use for collection-level resources (not item-level data).
|
|
200
|
+
- Common examples:
|
|
201
|
+
- `thumbnail` → preview image
|
|
202
|
+
- `overview` → documentation PDF
|
|
203
|
+
- Include:
|
|
204
|
+
- `href` (URL)
|
|
205
|
+
- `type` (MIME type, e.g. `image/png`)
|
|
206
|
+
- `roles` (e.g. `thumbnail`, `overview`)
|
|
207
|
+
- If no assets are needed, use an empty object:
|
|
208
|
+
```json
|
|
209
|
+
"assets": {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
### collection_config.json
|
|
213
|
+
|
|
214
|
+
This controls how Item metadata generation behaves. Base example in [EO.XXX.YYY.ZZZ/metadata/collection_config.json](EO.XXX.YYY.ZZZ/metadata/collection_config.json).
|
|
215
|
+
|
|
216
|
+
## Configuration Options
|
|
217
|
+
|
|
218
|
+
The following options are user-facing and supported during item generation:
|
|
219
|
+
|
|
220
|
+
| Key | Type | Default | Description |
|
|
221
|
+
|---|---|---|---|
|
|
222
|
+
| `id` | string | required | Collection id. Must match your collection root folder id. |
|
|
223
|
+
| `item_folder_level` | string | `DD` | Where item folders are discovered. Allowed values: `YYYY`, `MM`, `DD`, `NONE`. |
|
|
224
|
+
| `item_config_optional` | boolean | `false` | If `false`, each item folder is expected to contain `item_config.json`. |
|
|
225
|
+
| `item_asset_ignore_list` | list[string] | `[]` | File names to ignore when creating STAC assets. |
|
|
226
|
+
| `media_type_overrides` | object | `{}` | Optional extension to MIME type map used before default detection, e.g. `{ ".grib2": "application/x-grib2" }`. |
|
|
227
|
+
| `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. |
|
|
228
|
+
| `thumbnail_regex` | string | unset | Regex used to detect thumbnail assets and assign thumbnail role. |
|
|
229
|
+
| `overview_regex` | string | unset | Regex used to detect overview assets and assign overview role. |
|
|
230
|
+
| `additional_property_keys` | list[string] | `[]` | Additional property suffix keys parsed from item folder names (standard mode). |
|
|
231
|
+
| `item_folder_naming_convention_type` | string | `standard` | Naming convention mode. Allowed values: `standard`, `non_standard_1`. |
|
|
232
|
+
| `properties` | object | `{}` | Additional properties merged into each generated Item. |
|
|
233
|
+
| `bbox` | list[number] | unset | Bounding box `[minx, miny, maxx, maxy]` applied to generated items. |
|
|
234
|
+
| `item_date_overide` | string | unset | Simplified mode override date in `YYYYMMDD` format. |
|
|
235
|
+
|
|
236
|
+
Important: the accepted key name is `item_date_overide` (spelling as shown).
|
|
237
|
+
|
|
238
|
+
## Folder Granularity Options
|
|
239
|
+
|
|
240
|
+
Set `item_folder_level` in `collection_config.json`.
|
|
241
|
+
|
|
242
|
+
1. `DD`: item folders are expected under `data/YYYY/MM/DD/<item_folder>`.
|
|
243
|
+
2. `MM`: item folders are expected under `data/YYYY/MM/<item_folder>`.
|
|
244
|
+
3. `YYYY`: item folders are expected under `data/YYYY/<item_folder>`.
|
|
245
|
+
4. `NONE`: simplified mode. Any folder directly under `data/` becomes an Item.
|
|
246
|
+
|
|
247
|
+
## Standard Mode Example
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"id": "EO.XXX.YYY.ZZZ",
|
|
252
|
+
"item_asset_ignore_list": ["item_config.json"],
|
|
253
|
+
"media_type_overrides": {
|
|
254
|
+
".grib2": "application/x-grib2"
|
|
255
|
+
},
|
|
256
|
+
"item_config_optional": false,
|
|
257
|
+
"item_folder_level": "DD",
|
|
258
|
+
"thumbnail_regex": "^thumbnail",
|
|
259
|
+
"overview_regex": "^overview",
|
|
260
|
+
"additional_property_keys": []
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Simplified Mode Example
|
|
265
|
+
|
|
266
|
+
Simplified mode should only be used with agreement from support.
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"id": "EO.XXX.YYY.ZZZ",
|
|
271
|
+
"item_asset_ignore_list": ["item_config.json"],
|
|
272
|
+
"item_folder_level": "NONE",
|
|
273
|
+
"item_date_overide": "20240101",
|
|
274
|
+
"thumbnail_regex": "^thumbnail",
|
|
275
|
+
"overview_regex": "^overview",
|
|
276
|
+
"additional_property_keys": [],
|
|
277
|
+
"bbox": [-10.0, 35.0, 10.0, 60.0]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Simplified mode datetime behavior:
|
|
282
|
+
1. If `item_date_overide` is set, that date is used for all generated Items.
|
|
283
|
+
2. Otherwise, if the first folder level under `data/` is a year, `YYYY-01-01` is used.
|
|
284
|
+
3. Otherwise, the generation date is used.
|
|
285
|
+
|
|
286
|
+
## Per-Item Configuration
|
|
287
|
+
|
|
288
|
+
Each item folder can include an `item_config.json` file. Typical per-item overrides:
|
|
289
|
+
1. `bbox`
|
|
290
|
+
2. `properties`
|
|
291
|
+
3. Asset-related regex fields when needed
|
|
292
|
+
|
|
293
|
+
When present, item-level values override collection-level values.
|
|
294
|
+
|
|
295
|
+
## Generate Item Metadata
|
|
296
|
+
|
|
297
|
+
### CLI Usage
|
|
298
|
+
|
|
299
|
+
Primary command (after installing the package):
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
generate-item-metadata EO.XXX.YYY.ZZZ
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
This default expects the collection at `./EO.XXX.YYY.ZZZ`.
|
|
306
|
+
|
|
307
|
+
Optional collection subfolder path:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
generate-item-metadata EO.XXX.YYY.ZZZ --collection-path toto
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
With this option, the collection is resolved at `./toto/EO.XXX.YYY.ZZZ`.
|
|
314
|
+
For safety, `--collection-path` is currently restricted to subfolders under the current working directory.
|
|
315
|
+
|
|
316
|
+
Optional bucket override:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
generate-item-metadata EO.XXX.YYY.ZZZ --bucket-name usergenerated-proposal-eo.xxx.yyy.zzz
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Compatibility command (run from the project root):
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
python generate_item_metadata.py EO.XXX.YYY.ZZZ
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Compatibility command with collection subfolder:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
python generate_item_metadata.py EO.XXX.YYY.ZZZ --collection-path toto
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Programmatic Usage
|
|
335
|
+
|
|
336
|
+
You can also use `ItemGenerator` directly in your Python scripts:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from usergenerated import ItemGenerator
|
|
340
|
+
|
|
341
|
+
# Create a generator for your collection
|
|
342
|
+
generator = ItemGenerator("EO.XXX.YYY.ZZZ")
|
|
343
|
+
|
|
344
|
+
# Generate Item metadata
|
|
345
|
+
generator.run()
|
|
346
|
+
|
|
347
|
+
# Resolve collection from ./toto/EO.XXX.YYY.ZZZ
|
|
348
|
+
generator_with_path = ItemGenerator("EO.XXX.YYY.ZZZ", collection_path="toto")
|
|
349
|
+
generator_with_path.run()
|
|
350
|
+
|
|
351
|
+
# With optional bucket name override
|
|
352
|
+
generator_with_bucket = ItemGenerator("EO.XXX.YYY.ZZZ", override_bucket_name="custom-bucket")
|
|
353
|
+
generator_with_bucket.run()
|
|
354
|
+
|
|
355
|
+
# Also write a reordered/normalised copy to output/collection_reordered.json (off by default)
|
|
356
|
+
generator_reordered = ItemGenerator("EO.XXX.YYY.ZZZ", save_reordered_collection=True)
|
|
357
|
+
generator_reordered.run()
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Generated output is written to:
|
|
361
|
+
|
|
362
|
+
```text
|
|
363
|
+
<COLLECTION_ID>/metadata/items/*.json
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The `ItemGenerator` class is the primary API for programmatic use. The CLI command is a thin wrapper around this class.
|
|
367
|
+
|
|
368
|
+
## Regenerate Item Metadata in S3
|
|
369
|
+
|
|
370
|
+
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.
|
|
371
|
+
|
|
372
|
+
### CLI Usage
|
|
373
|
+
|
|
374
|
+
Primary command (regenerate items in S3 for your collection):
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
This command:
|
|
381
|
+
1. Mirrors collection data from S3 to a local folder.
|
|
382
|
+
2. Regenerates STAC Item metadata locally with your configured settings.
|
|
383
|
+
3. Backs up existing S3 items by default.
|
|
384
|
+
4. Replaces only the `metadata/items` prefix in S3 (never modifies data files).
|
|
385
|
+
|
|
386
|
+
Optional dry-run (validate without modifying S3):
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --dry-run
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Optional bucket name override:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --bucket-name custom-bucket-name
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Optional local mirror folder (default: `tmp_structure_mirror`):
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --local-root "./my_mirror"
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Disable backup:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --no-backup-items
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Skip the `ready_for_transfer` safety check (see [Safety Behavior](#safety-behavior); enabled by default):
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --no-use-ready-for-transfer-flag
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Demonstration script usage (env-first configuration via `.env`/environment variables, see
|
|
417
|
+
`script_s3_regenerate_item_metadata.py` for the full list of `S3_*` variables):
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
python script_s3_regenerate_item_metadata.py
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
This script is intentionally a thin example of programmatic `ItemGeneratorS3` usage.
|
|
424
|
+
For routine operations, prefer the `regenerate-item-metadata-s3` console command.
|
|
425
|
+
|
|
426
|
+
The demo script additionally supports `USE_READY_FOR_TRANSFER_FLAG` (or the `use_ready_for_transfer_flag`
|
|
427
|
+
keyword argument to `run_regenerate_item_metadata`) to toggle the `ready_for_transfer` safety check described
|
|
428
|
+
below — see [Safety Behavior](#safety-behavior). The primary console command exposes the same toggle via
|
|
429
|
+
`--use-ready-for-transfer-flag`/`--no-use-ready-for-transfer-flag` (shown above).
|
|
430
|
+
|
|
431
|
+
### Programmatic Usage
|
|
432
|
+
|
|
433
|
+
You can also use `ItemGeneratorS3` directly in your Python scripts:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from usergenerated import ItemGeneratorS3
|
|
437
|
+
|
|
438
|
+
# Create a generator for your collection
|
|
439
|
+
generator = ItemGeneratorS3(
|
|
440
|
+
"EO.XXX.YYY.ZZZ",
|
|
441
|
+
dry_run=True, # Validate without modifying S3
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Regenerate Item metadata
|
|
445
|
+
generator.run()
|
|
446
|
+
|
|
447
|
+
# With explicit bucket name
|
|
448
|
+
generator_with_bucket = ItemGeneratorS3(
|
|
449
|
+
"EO.XXX.YYY.ZZZ",
|
|
450
|
+
bucket_name="custom-bucket",
|
|
451
|
+
backup_items=True,
|
|
452
|
+
)
|
|
453
|
+
generator_with_bucket.run()
|
|
454
|
+
|
|
455
|
+
# Skip the ready_for_transfer safety check (see Safety Behavior below); enabled by default
|
|
456
|
+
generator_without_gate = ItemGeneratorS3(
|
|
457
|
+
"EO.XXX.YYY.ZZZ",
|
|
458
|
+
use_ready_for_transfer_flag=False,
|
|
459
|
+
)
|
|
460
|
+
generator_without_gate.run()
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Safety Behavior
|
|
464
|
+
|
|
465
|
+
The regeneration workflow includes several safety measures:
|
|
466
|
+
|
|
467
|
+
1. Validates credentials and bucket structure before processing.
|
|
468
|
+
2. Mirrors S3 content locally and ensures local processing under `<collection_id>/`.
|
|
469
|
+
3. By default (`use_ready_for_transfer_flag=True`), requires `"ready_for_transfer": true` in the mirrored
|
|
470
|
+
collection's `metadata/collection_config.json`; aborts with an error if the field is missing or `false` (this
|
|
471
|
+
check applies even with `--dry-run`). Skip this check via `--no-use-ready-for-transfer-flag` on the CLI, the
|
|
472
|
+
`use_ready_for_transfer_flag=False` constructor argument, or `USE_READY_FOR_TRANSFER_FLAG=false` for the demo
|
|
473
|
+
script.
|
|
474
|
+
4. Regenerates Item metadata locally with existing generation logic.
|
|
475
|
+
5. Backs up existing `<collection_id>/metadata/items` by default (can be disabled).
|
|
476
|
+
6. Replaces only `<collection_id>/metadata/items` in S3 (never modifies `data/`).
|
|
477
|
+
7. With `--dry-run`, performs local validation and regeneration, then prints planned S3 actions without modifying the bucket.
|
|
478
|
+
|
|
479
|
+
### Mirroring Behavior
|
|
480
|
+
|
|
481
|
+
The mirroring process downloads real files selectively to improve performance:
|
|
482
|
+
|
|
483
|
+
1. Downloads real files only for `metadata/*` and `item_config.json` keys.
|
|
484
|
+
2. Writes per-folder manifest files (`usergenerated_s3_folder_manifest.jsonl`) for discovered objects.
|
|
485
|
+
3. Uses manifest entries during item generation so full non-metadata assets are not required locally.
|
|
486
|
+
4. Supports optional placeholder file creation for non-fetched objects (disabled in the S3 regeneration workflow).
|
|
487
|
+
5. Logs a summary with discovered, downloaded, and failed object counts, plus periodic progress logs during large scans.
|
|
488
|
+
|
|
489
|
+
## Maintainer Notes
|
|
490
|
+
|
|
491
|
+
Recent internal refactoring improved testability and packaging workflows.
|
|
492
|
+
|
|
493
|
+
1. The primary command is now `generate-item-metadata <COLLECTION_ID>`.
|
|
494
|
+
2. [generate_item_metadata.py](generate_item_metadata.py) is retained as a compatibility wrapper.
|
|
495
|
+
3. Internal logic was modularized into reusable helpers under `usergenerated/` for:
|
|
496
|
+
- collection path resolution
|
|
497
|
+
- item folder discovery by configured granularity
|
|
498
|
+
- item config precedence/loading
|
|
499
|
+
- simplified and hierarchy-based datetime resolution
|
|
500
|
+
4. This split enables unit-heavy testing of pure logic while keeping entry-point orchestration thin.
|
|
501
|
+
|
|
502
|
+
## Optional S3 Upload
|
|
503
|
+
|
|
504
|
+
Automatic upload is configured at runtime via `ItemGenerator` constructor parameters
|
|
505
|
+
or CLI flags (not via module constants).
|
|
506
|
+
|
|
507
|
+
Programmatic example:
|
|
508
|
+
|
|
509
|
+
```python
|
|
510
|
+
from usergenerated import ItemGenerator
|
|
511
|
+
|
|
512
|
+
generator = ItemGenerator(
|
|
513
|
+
"EO.XXX.YYY.ZZZ",
|
|
514
|
+
is_upload_s3=True,
|
|
515
|
+
s3_endpoint_url="https://s3.central.data.destination-earth.eu",
|
|
516
|
+
s3_user_generated_bucket_prefix="usergenerated-proposal",
|
|
517
|
+
)
|
|
518
|
+
generator.run()
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
CLI example:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
generate-item-metadata EO.XXX.YYY.ZZZ \
|
|
525
|
+
--upload-s3 \
|
|
526
|
+
--s3-endpoint-url "https://s3.central.data.destination-earth.eu" \
|
|
527
|
+
--s3-bucket-prefix "usergenerated-proposal"
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
If upload is enabled, create a `.env` file at the project root with credentials:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
AWS_ACCESS_KEY_ID="<your_access_key_id>"
|
|
534
|
+
AWS_SECRET_ACCESS_KEY="<your_secret_access_key>"
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
For manual upload workflows, see [README-s3cmd.md](README-s3cmd.md).
|
|
538
|
+
|
|
539
|
+
S3 utility wrappers are available for programmatic execution around `S3Tools`:
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
from script_s3_create_bucket import run_create_bucket
|
|
543
|
+
from script_s3_move_bucket_contents import run_move_bucket_contents
|
|
544
|
+
from script_s3_upload_file_to_bucket import run_upload_file
|
|
545
|
+
from script_s3_upload_folder_to_bucket import run_upload_folder
|
|
546
|
+
|
|
547
|
+
run_create_bucket(
|
|
548
|
+
aws_access_key_id="<key>",
|
|
549
|
+
aws_secret_access_key="<secret>",
|
|
550
|
+
s3_bucket_name="my-bucket",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
run_upload_file(
|
|
554
|
+
file_name="EO.XXX.YYY.ZZZ/metadata/collection.json",
|
|
555
|
+
object_name="EO.XXX.YYY.ZZZ/metadata/collection.json",
|
|
556
|
+
aws_access_key_id="<key>",
|
|
557
|
+
aws_secret_access_key="<secret>",
|
|
558
|
+
s3_bucket_name="my-bucket",
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
run_upload_folder(
|
|
562
|
+
folder_path="EO.XXX.YYY.ZZZ/metadata/items",
|
|
563
|
+
target_path="EO.XXX.YYY.ZZZ/metadata/items",
|
|
564
|
+
aws_access_key_id="<key>",
|
|
565
|
+
aws_secret_access_key="<secret>",
|
|
566
|
+
s3_bucket_name="my-bucket",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
run_move_bucket_contents(
|
|
570
|
+
aws_access_key_id="<key>",
|
|
571
|
+
aws_secret_access_key="<secret>",
|
|
572
|
+
s3_bucket_name="my-bucket",
|
|
573
|
+
s3_target_prefix="archive",
|
|
574
|
+
)
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### S3 Folder Upload Progress
|
|
578
|
+
|
|
579
|
+
When using `run_upload_folder(...)` or `S3Tools.upload_folder_to_s3(...)`, uploads now display progress bars:
|
|
580
|
+
|
|
581
|
+
1. A global `Total files` bar showing overall folder upload progress.
|
|
582
|
+
2. Per-file bars (bytes transferred) for active uploads.
|
|
583
|
+
|
|
584
|
+
Current behavior:
|
|
585
|
+
|
|
586
|
+
1. Uploads run concurrently with a bounded worker pool.
|
|
587
|
+
2. Per-file progress bars are capped to a visible range and positions are reused for large uploads.
|
|
588
|
+
3. `usergenerated_s3_folder_manifest.jsonl` files and symlinks are skipped from upload discovery.
|
|
589
|
+
4. A final summary logs discovered, uploaded, and failed file counts.
|
|
590
|
+
|
|
591
|
+
These wrappers accept explicit parameters first and only fall back to environment variables when a parameter is omitted.
|
|
592
|
+
|
|
593
|
+
**Regenerate Item Metadata**: Use the CLI command for routine operations (see [Regenerate Item Metadata in S3](#regenerate-item-metadata-in-s3)):
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
regenerate-item-metadata-s3 EO.XXX.YYY.ZZZ --dry-run
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
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.
|
|
600
|
+
|
|
601
|
+
### Copy a Collection Between Buckets
|
|
602
|
+
|
|
603
|
+
`script_s3_copy_source_bucket_collection_to_target_bucket_collection.py` copies a collection
|
|
604
|
+
folder from a source S3 bucket/account to a target S3 bucket/account, using
|
|
605
|
+
`usergenerated.s3.s3_collection_copier.CollectionCopier`. It:
|
|
606
|
+
|
|
607
|
+
- Detects the source bucket's `BucketLayout` (`COLLECTION_ROOT` or `FLAT_ROOT`) and normalizes
|
|
608
|
+
the target to always be `<collection_id>/...` (`COLLECTION_ROOT`), fixing up `FLAT_ROOT`
|
|
609
|
+
sources in the process.
|
|
610
|
+
- Never modifies the source bucket.
|
|
611
|
+
- Is safely re-runnable: objects already present in the target (by key) are skipped, so an
|
|
612
|
+
interrupted transfer can just be re-run to continue where it left off.
|
|
613
|
+
- Streams each object directly from source to target (no local disk buffering), since source and
|
|
614
|
+
target buckets are assumed to use different credentials/accounts.
|
|
615
|
+
|
|
616
|
+
Configure both buckets in `.env` (the source variables are the same ones used elsewhere in this
|
|
617
|
+
project; target variables are `TARGET_`-prefixed):
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
# Source bucket (existing variables, unchanged)
|
|
621
|
+
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
|
|
622
|
+
S3_BUCKET_NAME="my-source-bucket"
|
|
623
|
+
AWS_ACCESS_KEY_ID="<source_access_key_id>"
|
|
624
|
+
AWS_SECRET_ACCESS_KEY="<source_secret_access_key>"
|
|
625
|
+
|
|
626
|
+
# Target bucket
|
|
627
|
+
TARGET_S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
|
|
628
|
+
TARGET_S3_BUCKET_NAME="my-target-bucket"
|
|
629
|
+
TARGET_AWS_ACCESS_KEY_ID="<target_access_key_id>"
|
|
630
|
+
TARGET_AWS_SECRET_ACCESS_KEY="<target_secret_access_key>"
|
|
631
|
+
|
|
632
|
+
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
|
|
633
|
+
S3_FRESH_START="false" # set true to delete the target's existing '<collection_id>/' prefix first
|
|
634
|
+
S3_DRY_RUN="false" # set true to preview the copy plan without modifying the target
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Then run:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
python script_s3_copy_source_bucket_collection_to_target_bucket_collection.py
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
`S3_FRESH_START` is the only way to trigger the destructive "clean the target path first" step
|
|
644
|
+
(e.g. for a first run against a target that already has stray/partial contents); it is never
|
|
645
|
+
triggered automatically. Omit it (or leave it `false`) for the default incremental,
|
|
646
|
+
resume-friendly sync.
|
|
647
|
+
|
|
648
|
+
Folder uploads use object keys relative to the selected local folder. For example,
|
|
649
|
+
uploading `./EO.XXX.YYY.ZZZ/metadata/items` with no `target_path` uploads files like
|
|
650
|
+
`a.json` and `nested/b.json` at the bucket root. To place files under a collection or
|
|
651
|
+
metadata prefix in the bucket, pass that prefix explicitly as `target_path`.
|
|
652
|
+
|
|
653
|
+
### Verify a Collection Copy
|
|
654
|
+
|
|
655
|
+
`script_s3_verify_collection_copy.py` is a read-only smoke test to run right after the copy
|
|
656
|
+
script above completes: it uses `usergenerated.s3.s3_collection_copy_verifier.CollectionCopyVerifier`
|
|
657
|
+
to list the collection's objects in both buckets and confirms the target has exactly the same
|
|
658
|
+
relative keys as the source, with matching sizes for every key. Neither bucket is modified.
|
|
659
|
+
|
|
660
|
+
It reuses the same `.env` variables as the copy script above (no new variables needed):
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
python script_s3_verify_collection_copy.py
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The script exits `0` and prints `Collection copy verification passed: True` when the target
|
|
667
|
+
matches the source. On a mismatch `CollectionCopyVerifier.run()` raises `S3OperationError`
|
|
668
|
+
whose message summarises the missing, unexpected, and/or size-mismatched keys (sampled, up to
|
|
669
|
+
20 per category); the script reports that one line and exits `1`.
|
|
670
|
+
|
|
671
|
+
Similarly, `CollectionCopier.run()` (the copy script above) raises `S3OperationError` — naming
|
|
672
|
+
the failed-object count and a sample of failed keys — if any object fails to copy. Objects
|
|
673
|
+
that did copy are left in place, so re-running resumes from where it stopped.
|
|
674
|
+
|
|
675
|
+
S3 helper code lives under `usergenerated/s3/`.
|
|
676
|
+
|
|
677
|
+
### Promote a Collection (regenerate → copy → verify → read, in one call)
|
|
678
|
+
|
|
679
|
+
`usergenerated.s3.collection_promoter.promote_collection` (also
|
|
680
|
+
`from usergenerated import promote_collection`) runs the whole "a collection has been staged
|
|
681
|
+
in a source bucket; validate it, copy it to a target bucket, and hand me its items" workflow
|
|
682
|
+
as a single call, in strict order:
|
|
683
|
+
|
|
684
|
+
1. `ItemGeneratorS3(...).run()` — regenerate STAC item metadata **in the source bucket**
|
|
685
|
+
(source credentials; the `ready_for_transfer` safety gate stays on; `data/` is untouched).
|
|
686
|
+
2. `CollectionCopier(...).run()` — copy the collection source bucket → target bucket.
|
|
687
|
+
3. `CollectionCopyVerifier(...).run()` — confirm the target matches the source key-for-key.
|
|
688
|
+
4. Build a `PromotedCollection` read accessor over the **target** copy and call
|
|
689
|
+
`on_ready(handle)` **exactly once**.
|
|
690
|
+
|
|
691
|
+
`on_ready` is a callback you provide. Its argument exposes just two methods:
|
|
692
|
+
|
|
693
|
+
- `handle.read_collection() -> dict` — the target copy's `collection.json`
|
|
694
|
+
(wraps `get_collection_from_s3`).
|
|
695
|
+
- `handle.iter_items() -> Iterator[dict]` — the target copy's STAC Items, streamed one at a
|
|
696
|
+
time (wraps `iter_collection_items_from_s3`; lazy — consume it inside the callback).
|
|
697
|
+
|
|
698
|
+
Any domain-specific downstream work (database load, re-publish, indexing) stays entirely in
|
|
699
|
+
your `on_ready`; `promote_collection` owns no datastore and never returns a status — a normal
|
|
700
|
+
return means every stage passed and `on_ready` ran.
|
|
701
|
+
|
|
702
|
+
- `on_ready` runs **only after copy and verify have both succeeded**. Any earlier failure
|
|
703
|
+
raises (`S3OperationError` / `ConfigError` / `CredentialsError` / `FileNotFoundError`, and
|
|
704
|
+
`FileExistsError` from regeneration) and `on_ready` is never called.
|
|
705
|
+
- `dry_run=True` runs regeneration/validation only (via `ItemGeneratorS3(dry_run=True)`); the
|
|
706
|
+
copier and verifier are never constructed and `on_ready` is never called.
|
|
707
|
+
- `fresh_start=True` passes through to `CollectionCopier`, deleting the target
|
|
708
|
+
`<collection_id>/` prefix before copying. The source bucket is never modified.
|
|
709
|
+
- Idempotent / resumable: re-running after a failure repeats the skip-aware copy, re-verifies,
|
|
710
|
+
and re-invokes `on_ready`. Side effects are only on the source (item regen, plus a
|
|
711
|
+
timestamped `items` backup when `backup_items=True`) and the target (the copy).
|
|
712
|
+
|
|
713
|
+
```python
|
|
714
|
+
from usergenerated import PromotedCollection, promote_collection
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def on_ready(collection: PromotedCollection) -> None:
|
|
718
|
+
stac = collection.read_collection()
|
|
719
|
+
item_count = sum(1 for _ in collection.iter_items())
|
|
720
|
+
print(f"{stac['id']}: {item_count} item(s) ready in the target bucket")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
promote_collection(
|
|
724
|
+
collection_id="EO.XXX.YYY.ZZZ",
|
|
725
|
+
source_endpoint_url="https://s3.central.data.destination-earth.eu",
|
|
726
|
+
source_bucket_name="my-source-bucket",
|
|
727
|
+
source_aws_access_key_id="<source_access_key_id>",
|
|
728
|
+
source_aws_secret_access_key="<source_secret_access_key>",
|
|
729
|
+
target_endpoint_url="https://s3.central.data.destination-earth.eu",
|
|
730
|
+
target_bucket_name="my-target-bucket",
|
|
731
|
+
target_aws_access_key_id="<target_access_key_id>",
|
|
732
|
+
target_aws_secret_access_key="<target_secret_access_key>",
|
|
733
|
+
on_ready=on_ready,
|
|
734
|
+
)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`script_s3_promote_collection.py` is a runnable `.env`-driven demo whose `on_ready` logs the
|
|
738
|
+
promoted collection's id and item count. It reuses the same variables as the copy and
|
|
739
|
+
regenerate scripts (`S3_COLLECTION_ID`, `S3_ENDPOINT_URL`, `S3_BUCKET_NAME`,
|
|
740
|
+
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `TARGET_S3_ENDPOINT_URL`,
|
|
741
|
+
`TARGET_S3_BUCKET_NAME`, `TARGET_AWS_ACCESS_KEY_ID`, `TARGET_AWS_SECRET_ACCESS_KEY`,
|
|
742
|
+
`S3_DRY_RUN`, `S3_FRESH_START`), plus two optional keys read only by this script:
|
|
743
|
+
`S3_LOCAL_ROOT` (default `tmp_structure_mirror`) and `S3_BACKUP_ITEMS` (default `true`).
|
|
744
|
+
|
|
745
|
+
```bash
|
|
746
|
+
python script_s3_promote_collection.py
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Manipulate Collection Config in S3
|
|
750
|
+
|
|
751
|
+
`script_s3_collection_config_manipulation.py` demonstrates the helpers in
|
|
752
|
+
`usergenerated.s3.s3_collection_config_tools` for reading and updating a collection's
|
|
753
|
+
`collection_config.json` directly in S3, without a local mirror. Unlike the verify script above, it
|
|
754
|
+
**mutates** the live bucket. For a given collection it:
|
|
755
|
+
|
|
756
|
+
1. Resolves the config key with `resolve_collection_config_key` and prints the full `collection_config.json`.
|
|
757
|
+
2. Reads the `ready_for_transfer` flag via `get_ready_for_transfer`.
|
|
758
|
+
3. Sets `ready_for_transfer` to `false` via `set_ready_for_transfer`.
|
|
759
|
+
4. Adds an ad-hoc `last_transferred` field (current UTC time, ISO 8601) via `update_collection_config`.
|
|
760
|
+
|
|
761
|
+
Setting `ready_for_transfer` to `false` engages the `regenerate-item-metadata-s3` safety gate described
|
|
762
|
+
in [Safety Behavior](#safety-behavior) — a subsequent regeneration run will refuse to proceed until the
|
|
763
|
+
flag is set back to `true` (or the check is explicitly skipped).
|
|
764
|
+
|
|
765
|
+
For programmatic use the same module exposes two reusable building blocks so a caller does not have to
|
|
766
|
+
chain layout detection and a hand-written read-modify-write:
|
|
767
|
+
|
|
768
|
+
- `resolve_collection_config_key(s3_client, bucket_name, collection_id)` — locate `collection_config.json`
|
|
769
|
+
in the bucket without handling `BucketLayout` yourself.
|
|
770
|
+
- `update_collection_config(s3_client, bucket_name, key, changes)` — one read-modify-write for an
|
|
771
|
+
arbitrary set of fields. `changes` is either a mapping merged into the config (use the exported
|
|
772
|
+
`REMOVE` sentinel as a value to delete a key) or a callback that mutates the dict in place. Keys you
|
|
773
|
+
do not touch keep their value, order, and the file's `indent=4` formatting; it raises `ConfigError`
|
|
774
|
+
if the object is missing or not a JSON object and `S3OperationError` on an S3 read/write failure.
|
|
775
|
+
`set_ready_for_transfer` is a one-line wrapper over it.
|
|
776
|
+
|
|
777
|
+
Configure the target bucket in `.env` (the same single-bucket variables used by the regeneration demo
|
|
778
|
+
script):
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
|
|
782
|
+
S3_BUCKET_NAME="my-bucket"
|
|
783
|
+
AWS_ACCESS_KEY_ID="<access_key_id>"
|
|
784
|
+
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
|
|
785
|
+
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
Then run:
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
python script_s3_collection_config_manipulation.py
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Report Bucket Information
|
|
795
|
+
|
|
796
|
+
`script_s3_bucket_information.py` demonstrates `usergenerated.s3.s3_bucket_info.get_bucket_information`,
|
|
797
|
+
which makes a single paginated sweep of a bucket (or a prefix within it) and prints a JSON report:
|
|
798
|
+
file count, total and average size, the 5 largest files, oldest/most-recent modification times, a
|
|
799
|
+
count+size breakdown by top-level folder, a `generated_at` timestamp, and a sample file listing
|
|
800
|
+
capped at 50 entries (with a `sample_truncated` flag) so you never get thousands of rows back.
|
|
801
|
+
Every size in the report (`total_size_bytes`, `average_file_size_bytes`, and each `size_bytes` in
|
|
802
|
+
`largest_files`, `top_level_prefixes`, and `sample_files`) has a `..._human`/`size_human`
|
|
803
|
+
counterpart, e.g. `"total_size_human": "94.2 MB"`. It is **read-only** — the bucket is never
|
|
804
|
+
modified.
|
|
805
|
+
|
|
806
|
+
Writing the report to `output/<YYYYMMDDTHHMMSS>_Bucket_Information.json` (the timestamp matches
|
|
807
|
+
`generated_at` in the JSON) is **off by default** — the report is only printed to stdout. Turn it
|
|
808
|
+
on with either `S3_WRITE_OUTPUT_FILE="true"` in `.env`, or `write_output_file=True` when calling
|
|
809
|
+
`run_get_bucket_information` programmatically; the `output/` directory is created if needed.
|
|
810
|
+
|
|
811
|
+
Configure the bucket in `.env` (the same single-bucket variables used by the config manipulation
|
|
812
|
+
script above); `S3_PREFIX`, `S3_BUCKET_INFO_SAMPLE_LIMIT`, `S3_WRITE_OUTPUT_FILE`, and
|
|
813
|
+
`S3_BUCKET_INFO_OUTPUT_DIR` are optional:
|
|
814
|
+
|
|
815
|
+
```bash
|
|
816
|
+
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
|
|
817
|
+
S3_BUCKET_NAME="my-bucket"
|
|
818
|
+
AWS_ACCESS_KEY_ID="<access_key_id>"
|
|
819
|
+
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
|
|
820
|
+
S3_PREFIX="" # optional: scope the report to one folder/collection
|
|
821
|
+
S3_BUCKET_INFO_SAMPLE_LIMIT="50" # optional: cap on the sample file listing
|
|
822
|
+
S3_WRITE_OUTPUT_FILE="false" # optional: set true/1/yes/on to also write the JSON report to disk
|
|
823
|
+
S3_BUCKET_INFO_OUTPUT_DIR="output" # optional: where the timestamped JSON report is written
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
Then run:
|
|
827
|
+
|
|
828
|
+
```bash
|
|
829
|
+
python script_s3_bucket_information.py
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Copy Collection Metadata Locally
|
|
833
|
+
|
|
834
|
+
`script_s3_get_metadata.py` demonstrates
|
|
835
|
+
`usergenerated.s3.s3_collection_metadata_downloader.download_collection_metadata`, which copies a
|
|
836
|
+
collection's `metadata/` folder from S3 to a local destination. The source bucket's layout
|
|
837
|
+
(whether it holds `<collection_id>/metadata/` or a top-level `metadata/`) is detected
|
|
838
|
+
automatically, so the caller never needs to know or check it beforehand — the result always lands
|
|
839
|
+
under a folder named for the collection ID, e.g. `<destination_root>/EO.XXX.YYY.ZZZ/metadata`. It
|
|
840
|
+
is **read-only** with respect to S3 — nothing in the source bucket is modified.
|
|
841
|
+
|
|
842
|
+
The destination root is configurable via `S3_METADATA_DESTINATION_ROOT` (or the
|
|
843
|
+
`destination_root` argument when calling `run_get_metadata` programmatically); when unset, it
|
|
844
|
+
defaults to a fresh timestamped folder under `/tmp/usergenerated_tmp/`, e.g.
|
|
845
|
+
`/tmp/usergenerated_tmp/20260824T101500/EO.XXX.YYY.ZZZ/metadata`.
|
|
846
|
+
|
|
847
|
+
Objects are downloaded concurrently — one worker thread per object, bounded by the
|
|
848
|
+
`max_workers` argument (default `MAX_METADATA_DOWNLOAD_WORKERS` = 16, capped at 32). The single
|
|
849
|
+
`s3_client` is shared across those threads, so build it with a matching connection pool:
|
|
850
|
+
`create_s3_client(..., max_pool_connections=MAX_METADATA_DOWNLOAD_WORKERS)` (botocore defaults
|
|
851
|
+
to 10). If any object fails to download the whole call raises `S3OperationError` (a
|
|
852
|
+
`RuntimeError` subclass) rather than returning a partial folder; once a systemic problem has
|
|
853
|
+
failed ~20 objects it stops submitting
|
|
854
|
+
new downloads instead of grinding through every remaining key.
|
|
855
|
+
|
|
856
|
+
For callers that only need the files for the duration of a block — parse `collection.json`,
|
|
857
|
+
stream `items/` somewhere, done — use the `downloaded_collection_metadata` context manager
|
|
858
|
+
instead of calling `download_collection_metadata` directly; on exit it removes the
|
|
859
|
+
`<collection_id>/` subtree it downloaded, and if it also generated the timestamped destination
|
|
860
|
+
root (no explicit `destination_root` given) it removes that too, so nothing is left under
|
|
861
|
+
`/tmp`. An explicit `destination_root` you pass in is otherwise left in place:
|
|
862
|
+
|
|
863
|
+
```python
|
|
864
|
+
with downloaded_collection_metadata(s3_client, bucket, collection_id) as metadata_folder:
|
|
865
|
+
collection = get_stac_collection(metadata_folder)
|
|
866
|
+
for item in iter_items(metadata_folder):
|
|
867
|
+
...
|
|
868
|
+
# the timestamped /tmp/usergenerated_tmp/<...> directory is gone here
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
The same module also provides small supporting functions for reading the downloaded metadata:
|
|
872
|
+
`get_stac_collection`/`get_collection_config` load `collection.json`/`collection_config.json` as
|
|
873
|
+
dicts, and `iter_items`/`iter_item_batches` stream STAC Items out of the downloaded `items/`
|
|
874
|
+
folder — one item at a time, or in fixed-size batches — rather than loading every item into
|
|
875
|
+
memory at once. This matters for collections with very large item counts (hundreds of thousands),
|
|
876
|
+
where materializing every item as a single list would be memory-intensive. The batch size defaults
|
|
877
|
+
to `METADATA_DOWNLOADER_DEFAULT_ITEM_BATCH_SIZE` (500) and is configurable via
|
|
878
|
+
`S3_METADATA_ITEM_BATCH_SIZE` (or the `item_batch_size` argument to `run_get_metadata`).
|
|
879
|
+
|
|
880
|
+
If you only need to walk the items once and don't need them to persist,
|
|
881
|
+
`iter_collection_items_from_s3` / `iter_collection_item_batches_from_s3` (and
|
|
882
|
+
`get_collection_from_s3` for `collection.json`) stream STAC Items straight from the bucket with no
|
|
883
|
+
local copy written and no cleanup to do — a drop-in for `iter_items` / `iter_item_batches` /
|
|
884
|
+
`get_stac_collection`. The bucket layout is detected automatically, item bodies are fetched with a
|
|
885
|
+
bounded concurrent prefetch (`max_workers` defaults to `MAX_METADATA_DOWNLOAD_WORKERS`, capped at
|
|
886
|
+
32), and any fetch or parse failure raises `S3OperationError` rather than yielding a partial
|
|
887
|
+
result. Keep `download_collection_metadata` for when the files must stay on disk or be read more
|
|
888
|
+
than once. Setting `S3_METADATA_STREAM_ITEMS` to a truthy value (or passing `stream_items=True` to
|
|
889
|
+
`run_get_metadata`) runs `script_s3_get_metadata.py` on this no-download path.
|
|
890
|
+
|
|
891
|
+
Configure the collection and bucket in `.env` (the same single-bucket variables used by the config
|
|
892
|
+
manipulation script above); `S3_METADATA_DESTINATION_ROOT`, `S3_METADATA_ITEM_BATCH_SIZE`, and
|
|
893
|
+
`S3_METADATA_STREAM_ITEMS` are optional:
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
S3_COLLECTION_ID="EO.XXX.YYY.ZZZ"
|
|
897
|
+
S3_ENDPOINT_URL="https://s3.central.data.destination-earth.eu"
|
|
898
|
+
S3_BUCKET_NAME="my-bucket"
|
|
899
|
+
AWS_ACCESS_KEY_ID="<access_key_id>"
|
|
900
|
+
AWS_SECRET_ACCESS_KEY="<secret_access_key>"
|
|
901
|
+
S3_METADATA_DESTINATION_ROOT="" # optional: local folder to copy metadata/ into (default: /tmp/usergenerated_tmp/<timestamp>)
|
|
902
|
+
S3_METADATA_ITEM_BATCH_SIZE="" # optional: items per batch when streaming items/ (default: 500)
|
|
903
|
+
S3_METADATA_STREAM_ITEMS="" # optional: truthy = skip the local download, stream items straight from S3
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Then run:
|
|
907
|
+
|
|
908
|
+
```bash
|
|
909
|
+
python script_s3_get_metadata.py
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
## Runtime Initialization
|
|
913
|
+
|
|
914
|
+
Logging initialization is now explicit and idempotent.
|
|
915
|
+
|
|
916
|
+
1. Entry-point scripts call `setup_logging()` from `usergenerated.logging_config` at startup.
|
|
917
|
+
2. Environment loading (`load_dotenv`) is also called explicitly at startup.
|
|
918
|
+
3. Importing modules should not configure logging implicitly.
|
|
919
|
+
|
|
920
|
+
This keeps behavior stable regardless of import order and avoids duplicate log handlers.
|
|
921
|
+
|
|
922
|
+
### Exit codes and failure output
|
|
923
|
+
|
|
924
|
+
Every console command (`generate-item-metadata`, `regenerate-item-metadata-s3`) and root
|
|
925
|
+
`script_s3_*.py` shares one convention:
|
|
926
|
+
|
|
927
|
+
| Exit code | Meaning |
|
|
928
|
+
|---|---|
|
|
929
|
+
| `0` | success |
|
|
930
|
+
| `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 |
|
|
931
|
+
| `2` | command-line usage error (bad or missing arguments) |
|
|
932
|
+
|
|
933
|
+
## Troubleshooting
|
|
934
|
+
|
|
935
|
+
1. Generation fails with folder-level errors:
|
|
936
|
+
Set `item_folder_level` to match your actual `data/` layout.
|
|
937
|
+
|
|
938
|
+
2. Missing `item_config.json` errors:
|
|
939
|
+
Set `item_config_optional` to `true` if item config files are intentionally omitted.
|
|
940
|
+
|
|
941
|
+
3. Collection id validation errors:
|
|
942
|
+
Ensure root folder name and `collection_config.json > id` represent the same collection id.
|
|
943
|
+
|
|
944
|
+
4. Upload credential errors:
|
|
945
|
+
Ensure `.env` contains valid `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` when upload is enabled.
|
|
946
|
+
|
|
947
|
+
## External References
|
|
948
|
+
|
|
949
|
+
1. [DestinE Data Lake - Promote User Data to become DestinE data](https://destine-data-lake-docs.data.destination-earth.eu/en/latest/dedl-discovery-and-data-access/User-Generated-Data/Promote-user-data-to-become-DestinE-data/Promote-user-data-to-become-DestinE-data.html)
|
|
950
|
+
2. [STAC Collection specification](https://stacspec.org/en/about/stac-spec/)
|
|
951
|
+
3. [STAC Item specification](https://stacspec.org/en/about/stac-spec/)
|