anta-database 0.4.2__tar.gz → 0.5.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 (70) hide show
  1. {anta_database-0.4.2 → anta_database-0.5.0}/PKG-INFO +39 -11
  2. {anta_database-0.4.2 → anta_database-0.5.0}/README.md +22 -3
  3. anta_database-0.5.0/anta_database/__init__.py +68 -0
  4. anta_database-0.5.0/anta_database/cloud/__init__.py +24 -0
  5. anta_database-0.5.0/anta_database/cloud/manifest.py +98 -0
  6. anta_database-0.5.0/anta_database/cloud/publish.py +165 -0
  7. anta_database-0.5.0/anta_database/cloud/sync.py +239 -0
  8. anta_database-0.5.0/anta_database/compiling/__init__.py +17 -0
  9. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/compile.py +116 -156
  10. anta_database-0.5.0/anta_database/database/__init__.py +9 -0
  11. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/database/database.py +79 -7
  12. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/indexing/__init__.py +1 -1
  13. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/indexing/index_database.py +171 -20
  14. anta_database-0.5.0/anta_database/plotting/__init__.py +24 -0
  15. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/plotting/plotting.py +24 -51
  16. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/plotting/plotting_pism.py +45 -51
  17. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/PKG-INFO +39 -11
  18. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/SOURCES.txt +6 -14
  19. anta_database-0.5.0/anta_database.egg-info/entry_points.txt +3 -0
  20. anta_database-0.5.0/anta_database.egg-info/requires.txt +27 -0
  21. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/scm_file_list.json +28 -37
  22. anta_database-0.5.0/anta_database.egg-info/scm_version.json +8 -0
  23. {anta_database-0.4.2 → anta_database-0.5.0}/book/contact.md +3 -3
  24. {anta_database-0.4.2 → anta_database-0.5.0}/book/quick_start.ipynb +106 -86
  25. {anta_database-0.4.2 → anta_database-0.5.0}/pyproject.toml +30 -7
  26. anta_database-0.4.2/anta_database/__init__.py +0 -32
  27. anta_database-0.4.2/anta_database/compiling/__init__.py +0 -10
  28. anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.cpg +0 -1
  29. anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.dbf +0 -0
  30. anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.prj +0 -1
  31. anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.shp +0 -0
  32. anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.shx +0 -0
  33. anta_database-0.4.2/anta_database/data/centeroid_coords_basins.cpg +0 -1
  34. anta_database-0.4.2/anta_database/data/centeroid_coords_basins.dbf +0 -0
  35. anta_database-0.4.2/anta_database/data/centeroid_coords_basins.prj +0 -1
  36. anta_database-0.4.2/anta_database/data/centeroid_coords_basins.shp +0 -0
  37. anta_database-0.4.2/anta_database/data/centeroid_coords_basins.shx +0 -0
  38. anta_database-0.4.2/anta_database/data/convert_data.py +0 -19
  39. anta_database-0.4.2/anta_database/database/__init__.py +0 -9
  40. anta_database-0.4.2/anta_database/plotting/__init__.py +0 -10
  41. anta_database-0.4.2/anta_database/test_interactive.py +0 -38
  42. anta_database-0.4.2/anta_database/test_interactive_full.py +0 -89
  43. anta_database-0.4.2/anta_database.egg-info/requires.txt +0 -13
  44. anta_database-0.4.2/anta_database.egg-info/scm_version.json +0 -8
  45. {anta_database-0.4.2 → anta_database-0.5.0}/.github/workflows/deploy.yml +0 -0
  46. {anta_database-0.4.2 → anta_database-0.5.0}/.github/workflows/release.yml +0 -0
  47. {anta_database-0.4.2 → anta_database-0.5.0}/.gitignore +0 -0
  48. {anta_database-0.4.2 → anta_database-0.5.0}/LICENSE +0 -0
  49. {anta_database-0.4.2 → anta_database-0.5.0}/ROADMAP.md +0 -0
  50. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/reconnect.py +0 -0
  51. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/zarr_converter_simple.py +0 -0
  52. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/ANT_Basins_IMBIE2_v1.6.parquet +0 -0
  53. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/GL.parquet +0 -0
  54. /anta_database-0.4.2/anta_database/data/centeroid_coors_basins.parquet → /anta_database-0.5.0/anta_database/data/centeroid_coords_basins.parquet +0 -0
  55. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/ice-core-coordinates.csv +0 -0
  56. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/site-coords.parquet +0 -0
  57. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/figures/AntA_38ka.png +0 -0
  58. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/dependency_links.txt +0 -0
  59. {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/top_level.txt +0 -0
  60. {anta_database-0.4.2 → anta_database-0.5.0}/book/_config.yml +0 -0
  61. {anta_database-0.4.2 → anta_database-0.5.0}/book/_toc.yml +0 -0
  62. {anta_database-0.4.2 → anta_database-0.5.0}/book/advanced.md +0 -0
  63. {anta_database-0.4.2 → anta_database-0.5.0}/book/contribute.md +0 -0
  64. {anta_database-0.4.2 → anta_database-0.5.0}/book/figures/Overview_all.png +0 -0
  65. {anta_database-0.4.2 → anta_database-0.5.0}/book/intro.md +0 -0
  66. {anta_database-0.4.2 → anta_database-0.5.0}/book/logo.png +0 -0
  67. {anta_database-0.4.2 → anta_database-0.5.0}/book/pism_example.ipynb +0 -0
  68. {anta_database-0.4.2 → anta_database-0.5.0}/book/references.bib +0 -0
  69. {anta_database-0.4.2 → anta_database-0.5.0}/book/requirements.txt +0 -0
  70. {anta_database-0.4.2 → anta_database-0.5.0}/setup.cfg +0 -0
@@ -1,9 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anta_database
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: SQLite database for the AntArchitecture Community Data
5
- Author: Antoine Hermant
6
- Author-email: antoine.hermant@etik.com
5
+ Author-email: Antoine Hermant <antoine.hermant@etik.com>
7
6
  License-Expression: MIT
8
7
  Project-URL: Homepage, https://antoinehermant.github.io/anta_database/intro
9
8
  Project-URL: Documentation, https://antoinehermant.github.io/anta_database/quick_start
@@ -17,19 +16,29 @@ Classifier: Programming Language :: Python
17
16
  Requires-Python: >=3.11
18
17
  Description-Content-Type: text/markdown
19
18
  License-File: LICENSE
19
+ Requires-Dist: numpy
20
20
  Requires-Dist: pandas
21
- Requires-Dist: geopandas
22
- Requires-Dist: fastparquet
23
21
  Requires-Dist: pyarrow
24
22
  Requires-Dist: xarray
25
23
  Requires-Dist: h5netcdf
26
24
  Requires-Dist: h5py
27
- Requires-Dist: pyproj
28
- Requires-Dist: scipy
29
25
  Requires-Dist: matplotlib
30
26
  Requires-Dist: colormaps
31
27
  Requires-Dist: tqdm
32
- Requires-Dist: pyshp
28
+ Requires-Dist: shapely
29
+ Provides-Extra: compile
30
+ Requires-Dist: geopandas; extra == "compile"
31
+ Requires-Dist: pyproj; extra == "compile"
32
+ Requires-Dist: scipy; extra == "compile"
33
+ Requires-Dist: netcdf4; extra == "compile"
34
+ Requires-Dist: zarr; extra == "compile"
35
+ Provides-Extra: pism
36
+ Requires-Dist: scipy; extra == "pism"
37
+ Requires-Dist: netcdf4; extra == "pism"
38
+ Provides-Extra: publish
39
+ Requires-Dist: boto3>=1.36; extra == "publish"
40
+ Provides-Extra: all
41
+ Requires-Dist: anta_database[compile,pism,publish]; extra == "all"
33
42
  Dynamic: license-file
34
43
 
35
44
  <h1 align="center">
@@ -74,18 +83,37 @@ Each dataset contains (when available):
74
83
  pip install anta_database
75
84
  ```
76
85
 
86
+ This installs what is needed to query and plot the database. Optional extras:
87
+ ```bash
88
+ pip install "anta_database[compile]" # compile the database from raw data
89
+ pip install "anta_database[pism]" # plot comparisons with PISM outputs
90
+ pip install "anta_database[all]" # everything
91
+ ```
92
+
77
93
  For the latest development version:
78
94
  ```bash
79
95
  pip install git+https://github.com/antoinehermant/anta_database.git
80
96
  ```
81
97
 
98
+ ## Downloading the data
99
+
100
+ Download the database, or update it to the latest published version (only new or changed files are downloaded):
101
+ ```python
102
+ from anta_database import sync
103
+ sync() # default local directory
104
+ sync("/path/to/AntADatabase/") # or a directory of your choice
105
+ ```
106
+ or from the command line: `anta-sync --dir /path/to/AntADatabase/`. From an existing `Database`, use `db.sync()`, or `Database(sync=True)` to always sync when loading.
107
+
108
+ When loading a synced database, `Database` tells you if a newer version is published (disable with `check_updates=False`). Updates are never downloaded without you asking, so your results only change when you decide to sync.
109
+
82
110
  ## Quick Start
83
111
 
84
112
  ```python
85
113
  from anta_database import Database
86
114
 
87
- # Initialize database (contact maintainer for data access)
88
- db = Database("/path/to/AntADatabase/")
115
+ # Load the database (without a path, uses the default directory of sync())
116
+ db = Database()
89
117
 
90
118
  # Query data by various criteria
91
119
  db.query(dataset="Cavitte_2020", age=10000)
@@ -100,7 +128,7 @@ Full documentation and examples are available on the [Home Page](https://antoine
100
128
 
101
129
  ## Data Access
102
130
 
103
- The database files are not publicly hosted. Please contact [antoine.hermant@unibe.ch](mailto:antoine.hermant@unibe.ch) to request access to the data.
131
+ The AntADatabase is free to download with `sync()` (see [Downloading the data](#downloading-the-data)). Any data used must be cited at source using the DOIs provided in the metadata.
104
132
 
105
133
  ## Support
106
134
 
@@ -40,18 +40,37 @@ Each dataset contains (when available):
40
40
  pip install anta_database
41
41
  ```
42
42
 
43
+ This installs what is needed to query and plot the database. Optional extras:
44
+ ```bash
45
+ pip install "anta_database[compile]" # compile the database from raw data
46
+ pip install "anta_database[pism]" # plot comparisons with PISM outputs
47
+ pip install "anta_database[all]" # everything
48
+ ```
49
+
43
50
  For the latest development version:
44
51
  ```bash
45
52
  pip install git+https://github.com/antoinehermant/anta_database.git
46
53
  ```
47
54
 
55
+ ## Downloading the data
56
+
57
+ Download the database, or update it to the latest published version (only new or changed files are downloaded):
58
+ ```python
59
+ from anta_database import sync
60
+ sync() # default local directory
61
+ sync("/path/to/AntADatabase/") # or a directory of your choice
62
+ ```
63
+ or from the command line: `anta-sync --dir /path/to/AntADatabase/`. From an existing `Database`, use `db.sync()`, or `Database(sync=True)` to always sync when loading.
64
+
65
+ When loading a synced database, `Database` tells you if a newer version is published (disable with `check_updates=False`). Updates are never downloaded without you asking, so your results only change when you decide to sync.
66
+
48
67
  ## Quick Start
49
68
 
50
69
  ```python
51
70
  from anta_database import Database
52
71
 
53
- # Initialize database (contact maintainer for data access)
54
- db = Database("/path/to/AntADatabase/")
72
+ # Load the database (without a path, uses the default directory of sync())
73
+ db = Database()
55
74
 
56
75
  # Query data by various criteria
57
76
  db.query(dataset="Cavitte_2020", age=10000)
@@ -66,7 +85,7 @@ Full documentation and examples are available on the [Home Page](https://antoine
66
85
 
67
86
  ## Data Access
68
87
 
69
- The database files are not publicly hosted. Please contact [antoine.hermant@unibe.ch](mailto:antoine.hermant@unibe.ch) to request access to the data.
88
+ The AntADatabase is free to download with `sync()` (see [Downloading the data](#downloading-the-data)). Any data used must be cited at source using the DOIs provided in the metadata.
70
89
 
71
90
  ## Support
72
91
 
@@ -0,0 +1,68 @@
1
+ """
2
+ Anta Database Package
3
+
4
+ Main package for isochrone database access and processing.
5
+
6
+ The query, indexing and plotting tools only need the core dependencies.
7
+ Compiling and PISM plotting need optional extras and are imported lazily:
8
+ pip install "anta_database[compile]"
9
+ pip install "anta_database[pism]"
10
+
11
+ Download or update the published database with anta_database.sync().
12
+ """
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ # Database modules
17
+ from anta_database.database.database import Database, MetadataResult
18
+
19
+ # Indexing modules
20
+ from anta_database.indexing.index_database import IndexDatabase
21
+
22
+ # Plotting modules
23
+ from anta_database.plotting.plotting import Plotting
24
+
25
+ # Downloading and updating the published database
26
+ from anta_database.cloud.sync import sync
27
+
28
+ if TYPE_CHECKING:
29
+ from anta_database.compiling.zarr_converter_simple import FlightLineZarrConverter
30
+ from anta_database.compiling.compile import CompileDatabase
31
+ from anta_database.plotting.plotting_pism import PISMPlotting
32
+
33
+ # Optional classes, imported on first access
34
+ _LAZY_IMPORTS = {
35
+ "FlightLineZarrConverter": "anta_database.compiling",
36
+ "CompileDatabase": "anta_database.compiling",
37
+ "PISMPlotting": "anta_database.plotting.plotting_pism",
38
+ }
39
+
40
+
41
+ def __getattr__(name):
42
+ if name in _LAZY_IMPORTS:
43
+ import importlib
44
+
45
+ module = importlib.import_module(_LAZY_IMPORTS[name])
46
+ return getattr(module, name)
47
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
48
+
49
+
50
+ def __dir__():
51
+ return sorted(list(globals()) + list(_LAZY_IMPORTS))
52
+
53
+
54
+ __all__ = [
55
+ # Database classes
56
+ "Database",
57
+ "MetadataResult",
58
+ # Compiling classes
59
+ "FlightLineZarrConverter",
60
+ "CompileDatabase",
61
+ # Indexing functions
62
+ "IndexDatabase",
63
+ # Plotting functions
64
+ "Plotting",
65
+ "PISMPlotting",
66
+ # Cloud functions
67
+ "sync",
68
+ ]
@@ -0,0 +1,24 @@
1
+ """
2
+ Cloud distribution of the AntADatabase.
3
+
4
+ sync() only needs the standard library. publish() is for maintainers and needs
5
+ the [publish] extra: pip install "anta_database[publish]"
6
+ """
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ from anta_database.cloud.sync import default_database_dir, sync
11
+
12
+ if TYPE_CHECKING:
13
+ from anta_database.cloud.publish import publish
14
+
15
+
16
+ def __getattr__(name):
17
+ if name == "publish":
18
+ from anta_database.cloud.publish import publish
19
+
20
+ return publish
21
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
22
+
23
+
24
+ __all__ = ["sync", "publish", "default_database_dir"]
@@ -0,0 +1,98 @@
1
+ """
2
+ Manifest shared by publish (maintainer) and sync (users).
3
+
4
+ A manifest lists every file of a published database with its checksum:
5
+ {
6
+ "format": 1,
7
+ "db_version": "2026.10.27-1530",
8
+ "created": "2026-10-27T15:30:00+00:00",
9
+ "files": {"AntADatabase.db": {"sha256": "...", "size": 123}, ...}
10
+ }
11
+ Paths are relative to the database directory and always use "/".
12
+ """
13
+
14
+ import glob
15
+ import hashlib
16
+ import json
17
+ import os
18
+ from datetime import datetime, timezone
19
+ from typing import Dict, Optional
20
+
21
+ MANIFEST_NAME = "manifest.json"
22
+ MANIFEST_FORMAT = 1
23
+ DB_FILE = "AntADatabase.db"
24
+
25
+ # Files of a database directory that are published, relative to the directory.
26
+ # Raw input data and temporary compile files are not published.
27
+ PUBLISHED_PATTERNS = [
28
+ DB_FILE,
29
+ "*/raw_md.json",
30
+ "*/h5/*.h5",
31
+ ]
32
+
33
+
34
+ def sha256sum(path: str, chunk_size: int = 1 << 20) -> str:
35
+ h = hashlib.sha256()
36
+ with open(path, "rb") as f:
37
+ for chunk in iter(lambda: f.read(chunk_size), b""):
38
+ h.update(chunk)
39
+ return h.hexdigest()
40
+
41
+
42
+ def list_published_files(database_dir: str) -> list:
43
+ """Relative paths of all files to publish from a local database directory."""
44
+ files = set()
45
+ for pattern in PUBLISHED_PATTERNS:
46
+ for path in glob.glob(os.path.join(database_dir, pattern)):
47
+ if os.path.isfile(path):
48
+ rel = os.path.relpath(path, database_dir)
49
+ files.add(rel.replace(os.sep, "/"))
50
+ return sorted(files)
51
+
52
+
53
+ def build_manifest(database_dir: str, db_version: Optional[str] = None) -> Dict:
54
+ """Hash all published files of a local database directory."""
55
+ from tqdm import tqdm
56
+
57
+ if not os.path.isfile(os.path.join(database_dir, DB_FILE)):
58
+ raise FileNotFoundError(f"{DB_FILE} not found in {database_dir}")
59
+
60
+ now = datetime.now(timezone.utc)
61
+ files = {}
62
+ for rel in tqdm(list_published_files(database_dir), desc="Hashing", unit="file"):
63
+ path = os.path.join(database_dir, rel)
64
+ files[rel] = {"sha256": sha256sum(path), "size": os.path.getsize(path)}
65
+
66
+ return {
67
+ "format": MANIFEST_FORMAT,
68
+ "db_version": db_version or now.strftime("%Y.%m.%d-%H%M"),
69
+ "created": now.isoformat(timespec="seconds"),
70
+ "files": files,
71
+ }
72
+
73
+
74
+ def check_manifest(manifest: Dict) -> Dict:
75
+ if manifest.get("format") != MANIFEST_FORMAT:
76
+ raise ValueError(
77
+ f"Unsupported manifest format {manifest.get('format')!r}. "
78
+ "Try upgrading anta_database: pip install -U anta_database"
79
+ )
80
+ for rel in manifest["files"]:
81
+ # Never let a manifest write outside the database directory
82
+ if rel.startswith("/") or ".." in rel.split("/") or "\\" in rel:
83
+ raise ValueError(f"Invalid path in manifest: {rel!r}")
84
+ return manifest
85
+
86
+
87
+ def read_manifest(path: str) -> Optional[Dict]:
88
+ if not os.path.isfile(path):
89
+ return None
90
+ with open(path) as f:
91
+ return json.load(f)
92
+
93
+
94
+ def write_manifest(manifest: Dict, path: str) -> None:
95
+ tmp = f"{path}.part"
96
+ with open(tmp, "w") as f:
97
+ json.dump(manifest, f, indent=1, sort_keys=True)
98
+ os.replace(tmp, path)
@@ -0,0 +1,165 @@
1
+ """
2
+ Publish a local AntADatabase to an S3-compatible bucket (maintainers only).
3
+
4
+ Needs the [publish] extra: pip install "anta_database[publish]"
5
+
6
+ Credentials are read by boto3 from the environment (AWS_ACCESS_KEY_ID,
7
+ AWS_SECRET_ACCESS_KEY) or from ~/.aws/credentials, never from the package.
8
+ The bucket must be publicly readable for sync() to work. For Infomaniak
9
+ Public Cloud, the S3 endpoint is https://s3.pub1.infomaniak.cloud.
10
+
11
+ anta-publish /path/to/AntADatabase --bucket antadatabase --prefix v1
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import mimetypes
17
+ import os
18
+ from typing import Dict, Optional
19
+
20
+ from tqdm import tqdm
21
+
22
+ from anta_database.cloud.manifest import MANIFEST_NAME, build_manifest, check_manifest
23
+
24
+ try:
25
+ import boto3
26
+ from botocore.config import Config
27
+ from botocore.exceptions import ClientError
28
+ except ImportError as e:
29
+ raise ImportError(
30
+ "Publishing the AntADatabase needs optional dependencies. "
31
+ 'Install them with: pip install "anta_database[publish]"'
32
+ ) from e
33
+
34
+ DEFAULT_ENDPOINT = "https://s3.pub1.infomaniak.cloud"
35
+
36
+
37
+ def _key(prefix: str, rel: str) -> str:
38
+ return f"{prefix.strip('/')}/{rel}" if prefix.strip("/") else rel
39
+
40
+
41
+ def _get_remote_manifest(s3, bucket: str, prefix: str) -> Optional[Dict]:
42
+ try:
43
+ obj = s3.get_object(Bucket=bucket, Key=_key(prefix, MANIFEST_NAME))
44
+ except ClientError as e:
45
+ if e.response["Error"]["Code"] in ("NoSuchKey", "404"):
46
+ return None
47
+ raise
48
+ return check_manifest(json.load(obj["Body"]))
49
+
50
+
51
+ def publish(
52
+ database_dir: str,
53
+ bucket: str,
54
+ prefix: str = "v1",
55
+ endpoint_url: Optional[str] = None,
56
+ db_version: Optional[str] = None,
57
+ dry_run: bool = False,
58
+ delete: bool = True,
59
+ ) -> Dict:
60
+ """
61
+ Upload new and changed files of a local database, then the manifest.
62
+
63
+ The manifest is uploaded after all data files, so users never sync a
64
+ manifest that points to missing files. A user syncing during a publish may
65
+ hit a checksum error on a file being replaced and simply has to sync again.
66
+ Files that are no longer part of the database are deleted afterwards.
67
+
68
+ Args:
69
+ database_dir: Local database directory (with AntADatabase.db).
70
+ bucket: Bucket name.
71
+ prefix: Key prefix inside the bucket, e.g. "v1".
72
+ endpoint_url: S3 endpoint. Defaults to $AWS_ENDPOINT_URL or Infomaniak.
73
+ db_version: Version label stored in the manifest. Defaults to the UTC date and time.
74
+ dry_run: Only report what would be uploaded and deleted.
75
+ delete: Delete objects that were published before but are no longer in the database.
76
+
77
+ Returns:
78
+ A dict with "db_version" and the lists "uploaded" and "deleted".
79
+ """
80
+ s3 = boto3.client(
81
+ "s3",
82
+ endpoint_url=endpoint_url or os.environ.get("AWS_ENDPOINT_URL", DEFAULT_ENDPOINT),
83
+ # Swift-based S3 APIs (Infomaniak) reject the default aws-chunked checksummed uploads
84
+ config=Config(
85
+ request_checksum_calculation="when_required",
86
+ response_checksum_validation="when_required",
87
+ ),
88
+ )
89
+
90
+ manifest = build_manifest(database_dir, db_version)
91
+ remote = _get_remote_manifest(s3, bucket, prefix) or {"files": {}}
92
+
93
+ to_upload = [
94
+ rel
95
+ for rel, entry in manifest["files"].items()
96
+ if remote["files"].get(rel, {}).get("sha256") != entry["sha256"]
97
+ ]
98
+ to_delete = sorted(set(remote["files"]) - set(manifest["files"])) if delete else []
99
+ result = {"db_version": manifest["db_version"], "uploaded": to_upload, "deleted": to_delete}
100
+ if dry_run:
101
+ return result
102
+
103
+ total = sum(manifest["files"][rel]["size"] for rel in to_upload)
104
+ with tqdm(total=total, unit="B", unit_scale=True, desc="Uploading") as progress:
105
+ for rel in to_upload:
106
+ content_type = mimetypes.guess_type(rel)[0] or "application/octet-stream"
107
+ s3.upload_file(
108
+ os.path.join(database_dir, rel),
109
+ bucket,
110
+ _key(prefix, rel),
111
+ ExtraArgs={"ContentType": content_type},
112
+ Callback=progress.update,
113
+ )
114
+
115
+ # Clients must never fetch a stale manifest from a cache
116
+ s3.put_object(
117
+ Bucket=bucket,
118
+ Key=_key(prefix, MANIFEST_NAME),
119
+ Body=json.dumps(manifest, indent=1, sort_keys=True).encode(),
120
+ ContentType="application/json",
121
+ CacheControl="no-cache",
122
+ )
123
+
124
+ for rel in to_delete:
125
+ s3.delete_object(Bucket=bucket, Key=_key(prefix, rel))
126
+
127
+ print(
128
+ f"Published AntADatabase {manifest['db_version']} to {bucket}/{prefix}: "
129
+ f"{len(to_upload)} file(s) uploaded, {len(to_delete)} deleted."
130
+ )
131
+ return result
132
+
133
+
134
+ def main(argv=None) -> None:
135
+ parser = argparse.ArgumentParser(
136
+ prog="anta-publish", description="Publish a local AntADatabase to an S3 bucket."
137
+ )
138
+ parser.add_argument("database_dir", help="Local database directory")
139
+ parser.add_argument("--bucket", required=True)
140
+ parser.add_argument("--prefix", default="v1")
141
+ parser.add_argument("--endpoint-url", help=f"S3 endpoint (default: $AWS_ENDPOINT_URL or {DEFAULT_ENDPOINT})")
142
+ parser.add_argument("--db-version", help="Version label (default: UTC date and time)")
143
+ parser.add_argument("--dry-run", action="store_true", help="Only show what would change")
144
+ parser.add_argument("--no-delete", action="store_true", help="Keep objects that are no longer in the database")
145
+ args = parser.parse_args(argv)
146
+
147
+ result = publish(
148
+ args.database_dir,
149
+ args.bucket,
150
+ prefix=args.prefix,
151
+ endpoint_url=args.endpoint_url,
152
+ db_version=args.db_version,
153
+ dry_run=args.dry_run,
154
+ delete=not args.no_delete,
155
+ )
156
+ if args.dry_run:
157
+ print(f"Would upload {len(result['uploaded'])} file(s), delete {len(result['deleted'])}.")
158
+ for rel in result["uploaded"]:
159
+ print(f" + {rel}")
160
+ for rel in result["deleted"]:
161
+ print(f" - {rel}")
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()