aboa 0.1.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 (47) hide show
  1. aboa-0.1.0/AUTHORS +5 -0
  2. aboa-0.1.0/LICENSE +19 -0
  3. aboa-0.1.0/MANIFEST.in +3 -0
  4. aboa-0.1.0/PKG-INFO +374 -0
  5. aboa-0.1.0/README.md +344 -0
  6. aboa-0.1.0/pyproject.toml +59 -0
  7. aboa-0.1.0/setup.cfg +4 -0
  8. aboa-0.1.0/src/aboa/__init__.py +4 -0
  9. aboa-0.1.0/src/aboa/config/archive_configurations.xml +8 -0
  10. aboa-0.1.0/src/aboa/config/datamodel.json +12 -0
  11. aboa-0.1.0/src/aboa/config/engine.json +10 -0
  12. aboa-0.1.0/src/aboa/datamodel/__init__.py +1 -0
  13. aboa-0.1.0/src/aboa/datamodel/aboa_data_model.sql +365 -0
  14. aboa-0.1.0/src/aboa/datamodel/archived_files.py +407 -0
  15. aboa-0.1.0/src/aboa/datamodel/base.py +22 -0
  16. aboa-0.1.0/src/aboa/datamodel/errors.py +23 -0
  17. aboa-0.1.0/src/aboa/datamodel/functions.py +40 -0
  18. aboa-0.1.0/src/aboa/engine/__init__.py +1 -0
  19. aboa-0.1.0/src/aboa/engine/commands.py +788 -0
  20. aboa-0.1.0/src/aboa/engine/engine.py +1508 -0
  21. aboa-0.1.0/src/aboa/engine/errors.py +64 -0
  22. aboa-0.1.0/src/aboa/engine/functions.py +207 -0
  23. aboa-0.1.0/src/aboa/engine/operators.py +18 -0
  24. aboa-0.1.0/src/aboa/engine/parsing.py +161 -0
  25. aboa-0.1.0/src/aboa/engine/query.py +619 -0
  26. aboa-0.1.0/src/aboa/engine/retention.py +155 -0
  27. aboa-0.1.0/src/aboa/engine/xpath_functions.py +32 -0
  28. aboa-0.1.0/src/aboa/logging.py +107 -0
  29. aboa-0.1.0/src/aboa/processors/__init__.py +1 -0
  30. aboa-0.1.0/src/aboa/processors/base_processor.py +41 -0
  31. aboa-0.1.0/src/aboa/schemas/aboa_archive_configurations.xsd +53 -0
  32. aboa-0.1.0/src/aboa/scripts/__init__.py +3 -0
  33. aboa-0.1.0/src/aboa/scripts/aboa_archive.py +17 -0
  34. aboa-0.1.0/src/aboa/scripts/aboa_clean_up.py +17 -0
  35. aboa-0.1.0/src/aboa/scripts/aboa_delete.py +17 -0
  36. aboa-0.1.0/src/aboa/scripts/aboa_init.py +143 -0
  37. aboa-0.1.0/src/aboa/scripts/aboa_init_ddbb.sh +107 -0
  38. aboa-0.1.0/src/aboa/scripts/aboa_recover.py +17 -0
  39. aboa-0.1.0/src/aboa/scripts/aboa_retrieve.py +17 -0
  40. aboa-0.1.0/src/aboa/scripts/initialize_aboa_ddbb.sh +20 -0
  41. aboa-0.1.0/src/aboa/scripts/start_aboa.sh +6 -0
  42. aboa-0.1.0/src/aboa.egg-info/PKG-INFO +374 -0
  43. aboa-0.1.0/src/aboa.egg-info/SOURCES.txt +45 -0
  44. aboa-0.1.0/src/aboa.egg-info/dependency_links.txt +1 -0
  45. aboa-0.1.0/src/aboa.egg-info/entry_points.txt +7 -0
  46. aboa-0.1.0/src/aboa.egg-info/requires.txt +8 -0
  47. aboa-0.1.0/src/aboa.egg-info/top_level.txt +1 -0
aboa-0.1.0/AUTHORS ADDED
@@ -0,0 +1,5 @@
1
+ ABOA was created by Daniel Brosnan Blázquez.
2
+
3
+ Major contributing authors include:
4
+
5
+ - Daniel Brosnan Blázquez
aboa-0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright 2026-2026 BOA authors and contributors <see AUTHORS file>.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
aboa-0.1.0/MANIFEST.in ADDED
@@ -0,0 +1,3 @@
1
+ include README.md LICENSE AUTHORS
2
+ prune src/tests
3
+ global-exclude __pycache__ *.py[cod] .coverage
aboa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,374 @@
1
+ Metadata-Version: 2.4
2
+ Name: aboa
3
+ Version: 0.1.0
4
+ Summary: Archive for Business Operations Analysis
5
+ Author: Daniel Brosnan Blázquez
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Daniel-Brosnan-Blazquez/aboa
8
+ Project-URL: Repository, https://github.com/Daniel-Brosnan-Blazquez/aboa
9
+ Project-URL: Issues, https://github.com/Daniel-Brosnan-Blazquez/aboa/issues
10
+ Keywords: archive,inventory,postgresql,retention
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: System :: Archiving
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ License-File: AUTHORS
22
+ Requires-Dist: sqlalchemy==1.3.22
23
+ Requires-Dist: psycopg2-binary==2.9.11
24
+ Requires-Dist: python-dateutil==2.9.0.post0
25
+ Requires-Dist: lxml==6.0.2
26
+ Provides-Extra: tests
27
+ Requires-Dist: pytest==8.4.2; extra == "tests"
28
+ Requires-Dist: pytest-cov==7.0.0; extra == "tests"
29
+ Dynamic: license-file
30
+
31
+ # ABOA
32
+
33
+ Archive for Business Operations Analysis.
34
+
35
+ ABOA stores files on a POSIX filesystem and keeps a PostgreSQL inventory of the
36
+ archived payloads, metadata, configuration history, root-directory history,
37
+ operation failures, trash queues, retention cleanup, and recovery state. The
38
+ current implementation exposes both command line tools and Python interfaces
39
+ through `aboa.engine.engine.Engine` and `aboa.engine.query.Query`.
40
+
41
+ ## Contents
42
+
43
+ - [What ABOA Provides](#what-aboa-provides)
44
+ - [Quick Start With Docker](#quick-start-with-docker)
45
+ - [Local Python Setup](#local-python-setup)
46
+ - [Install From PyPI](#install-from-pypi)
47
+ - [Runtime Configuration](#runtime-configuration)
48
+ - [Archive Configuration XML](#archive-configuration-xml)
49
+ - [Command Line Tools](#command-line-tools)
50
+ - [Python API](#python-api)
51
+ - [Operation Flow Schematics](#operation-flow-schematics)
52
+ - [Data Model](#data-model)
53
+ - [Development Layout](#development-layout)
54
+ - [Tests](#tests)
55
+ - [Publishing](#publishing)
56
+
57
+ ## What ABOA Provides
58
+
59
+ - Archive files into a managed root directory using configurable routing rules.
60
+ - Keep inventory metadata in SQLAlchemy/PostgreSQL tables.
61
+ - Track active archive roots and active XML archive-configuration history.
62
+ - Detect duplicate archived file names and route archive failures into the
63
+ internal `error` area when the input can still be brought under ABOA control.
64
+ - Retrieve inventory rows and optionally copy payloads to a destination folder.
65
+ - Logically delete files, move payloads to trash, permanently delete payloads, or
66
+ purge inventory entries when no managed payload remains.
67
+ - Recover files from trash or from logical-only deletion.
68
+ - Apply retention cleanup and final trash removal cleanup.
69
+ - Record failed operations in `archive_operations` while successful operations
70
+ are written only to the log.
71
+
72
+ ## Quick Start With Docker
73
+
74
+ The development compose file starts an ABOA container and a PostgreSQL/PostGIS
75
+ database container. The source checkout is mounted into the ABOA container at
76
+ `/aboa`.
77
+
78
+ ```bash
79
+ docker compose -f compose_dev.yml up -d --build
80
+ docker compose -f compose_dev.yml exec aboa initialize_aboa_ddbb.sh
81
+ docker compose -f compose_dev.yml exec aboa aboa_archive.py --file /aboa/src/tests/inputs/sample.txt
82
+ docker compose -f compose_dev.yml exec aboa aboa_retrieve.py --name sample.txt --list
83
+ ```
84
+
85
+ `initialize_aboa_ddbb.sh` calls `aboa_init.py -y` and recreates the configured
86
+ database from `src/aboa/datamodel/aboa_data_model.sql`, so it deletes existing ABOA
87
+ inventory data for that database.
88
+
89
+ ## Local Python Setup
90
+
91
+ For local development outside Docker, use Python 3.9 or newer and install from
92
+ the repository root:
93
+
94
+ ```bash
95
+ python3 -m venv .venv
96
+ source .venv/bin/activate
97
+ python -m pip install -e ".[tests]"
98
+ ```
99
+
100
+ Set the runtime paths before importing or running ABOA:
101
+
102
+ ```bash
103
+ export ABOA_RESOURCES_PATH="$PWD/src/aboa/config"
104
+ export ABOA_LOG_PATH="$PWD/log"
105
+ export ABOA_DEFAULT_ARCHIVE_PATH="/tmp/aboa_archive"
106
+ mkdir -p "$ABOA_LOG_PATH" "$ABOA_DEFAULT_ARCHIVE_PATH"
107
+ ```
108
+
109
+ ## Install From PyPI
110
+
111
+ Once the first release is published, install ABOA in a virtual environment on a
112
+ POSIX system with Python 3.9 or newer:
113
+
114
+ ```bash
115
+ python -m pip install aboa
116
+ ```
117
+
118
+ ABOA requires a PostgreSQL server and the PostgreSQL `psql` command to initialize
119
+ the database. Copy the bundled defaults to a writable configuration directory:
120
+
121
+ ```bash
122
+ export ABOA_RESOURCES_PATH="$PWD/aboa-config"
123
+ export ABOA_LOG_PATH="$PWD/log"
124
+ export ABOA_DEFAULT_ARCHIVE_PATH="$PWD/archive"
125
+ mkdir -p "$ABOA_RESOURCES_PATH" "$ABOA_LOG_PATH" "$ABOA_DEFAULT_ARCHIVE_PATH"
126
+ python -c 'import os, shutil, aboa; from pathlib import Path; shutil.copytree(Path(aboa.__file__).parent / "config", os.environ["ABOA_RESOURCES_PATH"], dirs_exist_ok=True)'
127
+ ```
128
+
129
+ Edit `datamodel.json` for the database connection and set `root_directory` in
130
+ `archive_configurations.xml` to the archive directory. Then run `aboa_init.py`
131
+ and follow its confirmation prompt. Initialization requires PostgreSQL
132
+ administrator access and recreates the configured database.
133
+
134
+ ## Runtime Configuration
135
+
136
+ ABOA reads JSON and XML runtime resources from `ABOA_RESOURCES_PATH`, falling
137
+ back to bundled defaults when it is unset. The XML schema is bundled with the
138
+ `aboa` package.
139
+
140
+ Required environment variables:
141
+
142
+ - `ABOA_LOG_PATH`: directory where rotating log files are written.
143
+
144
+ Optional environment variables:
145
+
146
+ - `ABOA_RESOURCES_PATH`: directory containing `datamodel.json`,
147
+ `engine.json`, and `archive_configurations.xml`; defaults to the packaged
148
+ configuration directory.
149
+ - `ABOA_DEFAULT_ARCHIVE_PATH`: fallback archive root used when the XML archive
150
+ configuration cannot be activated.
151
+ - `ABOA_DDBB_HOST`: overrides the host from `datamodel.json`.
152
+ - `ABOA_LOG_LEVEL`: overrides the log level from `engine.json`.
153
+ - `ABOA_STREAM_LOG`: enables stream logging in addition to the rotating file log.
154
+ - `ABOA_LOG_MAX_BYTES` and `ABOA_LOG_MAX_BACKUP`: override log rotation limits.
155
+
156
+ `src/aboa/config/engine.json` configures logging and archive cleanup behavior:
157
+
158
+ ```json
159
+ {
160
+ "LOG": {
161
+ "LEVEL": "INFO",
162
+ "MAX_BYTES": 50000000,
163
+ "MAX_BACKUP": 30
164
+ },
165
+ "ARCHIVE": {
166
+ "FINAL_REMOVAL_DELAY_DAYS": 30
167
+ }
168
+ }
169
+ ```
170
+
171
+ Archived payloads are stored below the active root using:
172
+
173
+ ```text
174
+ <root_directory>/<target_directory>/<YEAR>/<MONTH>/<DAY>/<file_name>
175
+ ```
176
+
177
+ The reserved target directories are `unknown`, `error`, and `trash`.
178
+
179
+ ## Archive Configuration XML
180
+
181
+ The default configuration lives at `src/aboa/config/archive_configurations.xml`.
182
+ ABOA validates this file with `src/aboa/schemas/aboa_archive_configurations.xsd`.
183
+
184
+ ```xml
185
+ <archive_configurations root_directory="/tmp/aboa_archive">
186
+ <archive_configuration file_group="invoices">
187
+ <file_mask>*.pdf</file_mask>
188
+ <file_directory>invoices</file_directory>
189
+ <retention_policy active="true" key="archive_date">P180D</retention_policy>
190
+ </archive_configuration>
191
+ <retention_policies>
192
+ <retention_policy active="true" key="validity_stop_date">P365D</retention_policy>
193
+ </retention_policies>
194
+ </archive_configurations>
195
+ ```
196
+
197
+ Archive rules are evaluated by file mask. A matching rule sets the target
198
+ directory, file group, optional processor module, and optional retention policy.
199
+ If no rule matches, ABOA stores the file in the internal `unknown` area with file
200
+ group `unknown`.
201
+
202
+ Processors are importable Python modules that expose `process(file_path)` and
203
+ return a metadata dictionary. Supported metadata includes file type, class,
204
+ version, validity dates, generation date, and expiration date.
205
+
206
+ Retention policies use XML duration values such as `P30D` and can use
207
+ `generation_date`, `reception_date`, `archive_date`, or `validity_stop_date` as
208
+ the base key.
209
+
210
+ ## Command Line Tools
211
+
212
+ The package installs these console commands:
213
+
214
+ ```bash
215
+ aboa_init.py [-f /path/to/aboa_data_model.sql] [-y]
216
+ aboa_archive.py --file /path/to/file [--delete] [--expiration-date DATETIME]
217
+ aboa_retrieve.py [filters] [--list] [--destination-path /path/to/output]
218
+ aboa_delete.py [filters] [--physical | --permanent] [--purge-entry] [--reason TEXT]
219
+ aboa_recover.py [archived-file filters | trash filters] [--list]
220
+ aboa_clean_up.py [--dry-run]
221
+ aboa_clean_up.py --final-removal [--dry-run]
222
+ aboa_clean_up.py --empty-trash [--dry-run]
223
+ ```
224
+
225
+ Common examples:
226
+
227
+ ```bash
228
+ aboa_archive.py --file /data/incoming/report.txt --delete
229
+ aboa_archive.py --file /data/incoming/invoice.pdf --expiration-date 2026-12-31T00:00:00
230
+ aboa_retrieve.py --name "%.txt" --order-by archive_date --descending --list
231
+ aboa_retrieve.py --uuid <file_uuid> --destination-path /tmp/retrieved
232
+ aboa_delete.py --uuid <file_uuid> --reason manual_delete
233
+ aboa_delete.py --uuid <file_uuid> --physical
234
+ aboa_delete.py --uuid <file_uuid> --permanent --purge-entry
235
+ aboa_recover.py --uuid <file_uuid>
236
+ aboa_recover.py --trash-uuid <file_to_remove_uuid>
237
+ aboa_clean_up.py --dry-run
238
+ aboa_clean_up.py --final-removal
239
+ ```
240
+
241
+ Retrieve, delete, and recover commands share many inventory filters, including:
242
+
243
+ - Text filters: `--uuid`, `--name`, `--path`, `--root-directory-uuid`,
244
+ `--file-group`, `--file-type`, `--file-class`, `--file-version`,
245
+ `--archive-configuration-uuid`, `--delete-archive-configuration-uuid`,
246
+ `--removal-justification`, and `--checksum`.
247
+ - Date filters: `--reception-date`, `--archive-date`, `--last-access-date`,
248
+ `--validity-start-date`, `--validity-stop-date`, `--generation-date`,
249
+ `--expiration-date`, and `--removal-date`.
250
+ - Numeric and boolean filters: `--file-size`, `--available`, and
251
+ `--physically-available`.
252
+ - Trash recovery filters: `--trash-uuid`, `--trash-path`,
253
+ `--trash-root-directory-uuid`, and `--trash-removal-date`.
254
+
255
+ Filter values may use operators such as `==`, `!=`, `<`, `<=`, `>`, and `>=`
256
+ for arithmetic/date filters. Text filters also expose `--<filter>-op` options
257
+ for text operators. Run any command with `--help` for the exact supported options.
258
+
259
+ ## Python API
260
+
261
+ Use `Engine` for mutating operations and `Query` for read-only inventory access.
262
+
263
+ ```python
264
+ from aboa.engine.engine import Engine
265
+ from aboa.engine.query import Query
266
+
267
+ engine = Engine()
268
+ try:
269
+ engine.archive_file("/data/incoming/report.txt", delete=True)
270
+ rows = engine.retrieve_files(
271
+ filters={"names": {"filter": "%.txt", "op": "like"}},
272
+ order_by={"field": "archive_date", "descending": True},
273
+ )
274
+ finally:
275
+ engine.close_session()
276
+
277
+ query = Query()
278
+ try:
279
+ active_root = query.get_active_root_directory()
280
+ failures = query.get_archive_operations(
281
+ operations={"filter": "archive", "op": "like"}
282
+ )
283
+ finally:
284
+ query.close_session()
285
+ ```
286
+
287
+ Important write-side methods include `archive_file`, `retrieve_files`,
288
+ `delete_files`, `delete_archived_file_entries`, `recover_files_from_trash`,
289
+ `move_archived_file_to_trash`, and `delete_archived_file_permanently`.
290
+
291
+ ## Operation Flow Schematics
292
+
293
+ ### Shared Operation Rules
294
+
295
+ ![ABOA shared operation rules](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_shared_operation_rules.svg)
296
+
297
+ ### Load Archive Configuration
298
+
299
+ ![ABOA load archive configuration flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_load_archive_configuration_flow.svg)
300
+
301
+ ### Archive Operation
302
+
303
+ ![ABOA archive operation flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_archive_operation_flow.svg)
304
+
305
+ ### Retrieve Operation
306
+
307
+ ![ABOA retrieve operation flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_retrieve_operation_flow.svg)
308
+
309
+ ### Delete Operation
310
+
311
+ ![ABOA delete operation flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_delete_operation_flow.svg)
312
+
313
+ ### Physical Deletion And Trash
314
+
315
+ ![ABOA physical deletion and trash flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_physical_deletion_trash_flow.svg)
316
+
317
+ ### Recovery Operation
318
+
319
+ ![ABOA recovery operation flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_recovery_operation_flow.svg)
320
+
321
+ ### Retention Cleanup
322
+
323
+ ![ABOA retention cleanup flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_retention_cleanup_flow.svg)
324
+
325
+ ### Final Removal Cleanup
326
+
327
+ ![ABOA final removal cleanup flow](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_final_removal_cleanup_flow.svg)
328
+
329
+ ## Data Model
330
+
331
+ The DDBB model, stored in `src/aboa/datamodel/aboa_data_model.dbm`, is built using pgModeler. The tool is then used to generate the SQL instructions, stored in `src/aboa/datamodel/aboa_data_model.sql`, to initialize the DDBB.
332
+
333
+ ![ABOA data model](https://raw.githubusercontent.com/Daniel-Brosnan-Blazquez/aboa/main/doc/fig/aboa_data_model.png)
334
+
335
+ Main inventory tables:
336
+
337
+ - `archive_root_directories`: active and historical archive roots.
338
+ - `archive_configurations`: active and historical XML configuration snapshots.
339
+ - `archived_files`: metadata for archived payloads and logical/physical
340
+ availability.
341
+ - `archive_operations`: failed operation records.
342
+ - `files_to_be_removed`: trash payloads queued for final removal.
343
+
344
+ ## Development Layout
345
+
346
+ - `src/aboa/datamodel`: SQLAlchemy model, database configuration, exported SQL,
347
+ and pgModeler model.
348
+ - `src/aboa/engine`: archive, query, configuration, retention, and CLI logic.
349
+ - `src/aboa/processors`: metadata processor contract.
350
+ - `src/aboa/config`: default runtime configuration.
351
+ - `src/aboa/schemas`: bundled XML schemas.
352
+ - `src/aboa/scripts`: console script entry points and database initialization helpers.
353
+ - `src/tests`: pytest-based test suite.
354
+ - `development_plans`: design notes, requirements, and operation flow details.
355
+ - `doc/fig`: data model and operation-flow diagrams.
356
+
357
+ ## Tests
358
+
359
+ Install the test extra and run the suite from the repository root:
360
+
361
+ ```bash
362
+ python -m pip install -e ".[tests]"
363
+ export ABOA_RESOURCES_PATH="$PWD/src/aboa/config"
364
+ export ABOA_LOG_PATH="$PWD/log"
365
+ export ABOA_DEFAULT_ARCHIVE_PATH="/tmp/aboa_archive"
366
+ mkdir -p "$ABOA_LOG_PATH" "$ABOA_DEFAULT_ARCHIVE_PATH"
367
+ python -m pytest src/tests
368
+ ```
369
+
370
+ ## Publishing
371
+
372
+ See [the publishing guide](https://github.com/Daniel-Brosnan-Blazquez/aboa/blob/main/doc/publishing.md)
373
+ for local build checks, PyPI Trusted Publisher setup, and the GitHub release
374
+ workflow. Package metadata and the release version live in `pyproject.toml`.