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.
Files changed (76) hide show
  1. usergenerated-2.0.0/PKG-INFO +951 -0
  2. usergenerated-2.0.0/README.md +925 -0
  3. usergenerated-2.0.0/generate_item_metadata.py +167 -0
  4. usergenerated-2.0.0/pyproject.toml +70 -0
  5. usergenerated-2.0.0/setup.cfg +4 -0
  6. usergenerated-2.0.0/tests/test_cli.py +214 -0
  7. usergenerated-2.0.0/tests/test_cli_common.py +48 -0
  8. usergenerated-2.0.0/tests/test_collection_paths.py +64 -0
  9. usergenerated-2.0.0/tests/test_collection_promoter.py +445 -0
  10. usergenerated-2.0.0/tests/test_confighelper.py +61 -0
  11. usergenerated-2.0.0/tests/test_confighelper_stac_io.py +30 -0
  12. usergenerated-2.0.0/tests/test_datetools.py +17 -0
  13. usergenerated-2.0.0/tests/test_env_utils.py +41 -0
  14. usergenerated-2.0.0/tests/test_errors.py +43 -0
  15. usergenerated-2.0.0/tests/test_generate_item_metadata_integration.py +370 -0
  16. usergenerated-2.0.0/tests/test_item_config_resolver.py +71 -0
  17. usergenerated-2.0.0/tests/test_item_datetime_resolution.py +85 -0
  18. usergenerated-2.0.0/tests/test_item_folder_discovery.py +65 -0
  19. usergenerated-2.0.0/tests/test_item_generator_get_item.py +159 -0
  20. usergenerated-2.0.0/tests/test_item_generator_run.py +205 -0
  21. usergenerated-2.0.0/tests/test_itemhelper_media_type.py +186 -0
  22. usergenerated-2.0.0/tests/test_logging_config.py +67 -0
  23. usergenerated-2.0.0/tests/test_s3_bucket_info.py +184 -0
  24. usergenerated-2.0.0/tests/test_s3_cli.py +166 -0
  25. usergenerated-2.0.0/tests/test_s3_collection_config_tools.py +324 -0
  26. usergenerated-2.0.0/tests/test_s3_collection_copier.py +289 -0
  27. usergenerated-2.0.0/tests/test_s3_collection_copy_verifier.py +178 -0
  28. usergenerated-2.0.0/tests/test_s3_collection_metadata_downloader.py +598 -0
  29. usergenerated-2.0.0/tests/test_s3_entrypoint.py +18 -0
  30. usergenerated-2.0.0/tests/test_s3_item_generator.py +765 -0
  31. usergenerated-2.0.0/tests/test_s3_mirror.py +99 -0
  32. usergenerated-2.0.0/tests/test_s3_mirror_should_fetch_real_file.py +18 -0
  33. usergenerated-2.0.0/tests/test_s3tools_upload_folder_target_path.py +521 -0
  34. usergenerated-2.0.0/tests/test_script_s3_bucket_information.py +267 -0
  35. usergenerated-2.0.0/tests/test_script_s3_collection_config_manipulation.py +160 -0
  36. usergenerated-2.0.0/tests/test_script_s3_copy_source_bucket_collection_to_target_bucket_collection.py +178 -0
  37. usergenerated-2.0.0/tests/test_script_s3_get_metadata.py +220 -0
  38. usergenerated-2.0.0/tests/test_script_s3_promote_collection.py +205 -0
  39. usergenerated-2.0.0/tests/test_script_s3_regenerate_item_metadata.py +168 -0
  40. usergenerated-2.0.0/tests/test_script_s3_utilities.py +396 -0
  41. usergenerated-2.0.0/tests/test_script_s3_verify_collection_copy.py +170 -0
  42. usergenerated-2.0.0/usergenerated/__init__.py +24 -0
  43. usergenerated-2.0.0/usergenerated/cli.py +80 -0
  44. usergenerated-2.0.0/usergenerated/cli_common.py +35 -0
  45. usergenerated-2.0.0/usergenerated/config/__init__.py +0 -0
  46. usergenerated-2.0.0/usergenerated/config/collection_paths.py +102 -0
  47. usergenerated-2.0.0/usergenerated/config/confighelper.py +362 -0
  48. usergenerated-2.0.0/usergenerated/config/constants.py +71 -0
  49. usergenerated-2.0.0/usergenerated/config/item_config_resolver.py +38 -0
  50. usergenerated-2.0.0/usergenerated/datetools.py +15 -0
  51. usergenerated-2.0.0/usergenerated/env_utils.py +43 -0
  52. usergenerated-2.0.0/usergenerated/errors.py +43 -0
  53. usergenerated-2.0.0/usergenerated/item/__init__.py +0 -0
  54. usergenerated-2.0.0/usergenerated/item/folder_discovery.py +44 -0
  55. usergenerated-2.0.0/usergenerated/item/itemhelper.py +365 -0
  56. usergenerated-2.0.0/usergenerated/item/path_datetime.py +79 -0
  57. usergenerated-2.0.0/usergenerated/item_generator.py +600 -0
  58. usergenerated-2.0.0/usergenerated/logging_config.py +75 -0
  59. usergenerated-2.0.0/usergenerated/s3/__init__.py +1 -0
  60. usergenerated-2.0.0/usergenerated/s3/collection_promoter.py +214 -0
  61. usergenerated-2.0.0/usergenerated/s3/s3_bucket_info.py +148 -0
  62. usergenerated-2.0.0/usergenerated/s3/s3_collection_config_tools.py +235 -0
  63. usergenerated-2.0.0/usergenerated/s3/s3_collection_copier.py +289 -0
  64. usergenerated-2.0.0/usergenerated/s3/s3_collection_copy_verifier.py +185 -0
  65. usergenerated-2.0.0/usergenerated/s3/s3_collection_metadata_downloader.py +495 -0
  66. usergenerated-2.0.0/usergenerated/s3/s3_mirror.py +404 -0
  67. usergenerated-2.0.0/usergenerated/s3/s3tools.py +570 -0
  68. usergenerated-2.0.0/usergenerated/s3_cli.py +50 -0
  69. usergenerated-2.0.0/usergenerated/s3_cli_common.py +66 -0
  70. usergenerated-2.0.0/usergenerated/s3_item_generator.py +370 -0
  71. usergenerated-2.0.0/usergenerated.egg-info/PKG-INFO +951 -0
  72. usergenerated-2.0.0/usergenerated.egg-info/SOURCES.txt +74 -0
  73. usergenerated-2.0.0/usergenerated.egg-info/dependency_links.txt +1 -0
  74. usergenerated-2.0.0/usergenerated.egg-info/entry_points.txt +3 -0
  75. usergenerated-2.0.0/usergenerated.egg-info/requires.txt +21 -0
  76. 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/)