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.
- {anta_database-0.4.2 → anta_database-0.5.0}/PKG-INFO +39 -11
- {anta_database-0.4.2 → anta_database-0.5.0}/README.md +22 -3
- anta_database-0.5.0/anta_database/__init__.py +68 -0
- anta_database-0.5.0/anta_database/cloud/__init__.py +24 -0
- anta_database-0.5.0/anta_database/cloud/manifest.py +98 -0
- anta_database-0.5.0/anta_database/cloud/publish.py +165 -0
- anta_database-0.5.0/anta_database/cloud/sync.py +239 -0
- anta_database-0.5.0/anta_database/compiling/__init__.py +17 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/compile.py +116 -156
- anta_database-0.5.0/anta_database/database/__init__.py +9 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/database/database.py +79 -7
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/indexing/__init__.py +1 -1
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/indexing/index_database.py +171 -20
- anta_database-0.5.0/anta_database/plotting/__init__.py +24 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/plotting/plotting.py +24 -51
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/plotting/plotting_pism.py +45 -51
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/PKG-INFO +39 -11
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/SOURCES.txt +6 -14
- anta_database-0.5.0/anta_database.egg-info/entry_points.txt +3 -0
- anta_database-0.5.0/anta_database.egg-info/requires.txt +27 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/scm_file_list.json +28 -37
- anta_database-0.5.0/anta_database.egg-info/scm_version.json +8 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/contact.md +3 -3
- {anta_database-0.4.2 → anta_database-0.5.0}/book/quick_start.ipynb +106 -86
- {anta_database-0.4.2 → anta_database-0.5.0}/pyproject.toml +30 -7
- anta_database-0.4.2/anta_database/__init__.py +0 -32
- anta_database-0.4.2/anta_database/compiling/__init__.py +0 -10
- anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.cpg +0 -1
- anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.dbf +0 -0
- anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.prj +0 -1
- anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.shp +0 -0
- anta_database-0.4.2/anta_database/data/ANT_Basins_IMBIE2_v1.6.shx +0 -0
- anta_database-0.4.2/anta_database/data/centeroid_coords_basins.cpg +0 -1
- anta_database-0.4.2/anta_database/data/centeroid_coords_basins.dbf +0 -0
- anta_database-0.4.2/anta_database/data/centeroid_coords_basins.prj +0 -1
- anta_database-0.4.2/anta_database/data/centeroid_coords_basins.shp +0 -0
- anta_database-0.4.2/anta_database/data/centeroid_coords_basins.shx +0 -0
- anta_database-0.4.2/anta_database/data/convert_data.py +0 -19
- anta_database-0.4.2/anta_database/database/__init__.py +0 -9
- anta_database-0.4.2/anta_database/plotting/__init__.py +0 -10
- anta_database-0.4.2/anta_database/test_interactive.py +0 -38
- anta_database-0.4.2/anta_database/test_interactive_full.py +0 -89
- anta_database-0.4.2/anta_database.egg-info/requires.txt +0 -13
- anta_database-0.4.2/anta_database.egg-info/scm_version.json +0 -8
- {anta_database-0.4.2 → anta_database-0.5.0}/.github/workflows/deploy.yml +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/.github/workflows/release.yml +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/.gitignore +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/LICENSE +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/ROADMAP.md +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/reconnect.py +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/compiling/zarr_converter_simple.py +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/ANT_Basins_IMBIE2_v1.6.parquet +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/GL.parquet +0 -0
- /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
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/ice-core-coordinates.csv +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/data/site-coords.parquet +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database/figures/AntA_38ka.png +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/dependency_links.txt +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/anta_database.egg-info/top_level.txt +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/_config.yml +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/_toc.yml +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/advanced.md +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/contribute.md +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/figures/Overview_all.png +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/intro.md +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/logo.png +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/pism_example.ipynb +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/references.bib +0 -0
- {anta_database-0.4.2 → anta_database-0.5.0}/book/requirements.txt +0 -0
- {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.
|
|
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:
|
|
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
|
-
#
|
|
88
|
-
db = Database(
|
|
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
|
|
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
|
-
#
|
|
54
|
-
db = Database(
|
|
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
|
|
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()
|