pymetadataeditor 0.3.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.
- pymetadataeditor-0.5.0/PKG-INFO +215 -0
- pymetadataeditor-0.5.0/README.md +186 -0
- pymetadataeditor-0.5.0/pymetadataeditor/__init__.py +35 -0
- pymetadataeditor-0.5.0/pymetadataeditor/api_client.py +76 -0
- pymetadataeditor-0.5.0/pymetadataeditor/api_types.py +21 -0
- pymetadataeditor-0.5.0/pymetadataeditor/codelists.py +229 -0
- pymetadataeditor-0.5.0/pymetadataeditor/collections.py +154 -0
- pymetadataeditor-0.5.0/pymetadataeditor/constants.py +5 -0
- pymetadataeditor-0.5.0/pymetadataeditor/data_structures.py +353 -0
- pymetadataeditor-0.5.0/pymetadataeditor/exceptions.py +90 -0
- pymetadataeditor-0.5.0/pymetadataeditor/indicators.py +454 -0
- pymetadataeditor-0.5.0/pymetadataeditor/interface.py +999 -0
- pymetadataeditor-0.5.0/pymetadataeditor/jobs.py +291 -0
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/llm_helpers.py +3 -3
- pymetadataeditor-0.5.0/pymetadataeditor/metadata_workflows.py +229 -0
- pymetadataeditor-0.5.0/pymetadataeditor/paging.py +75 -0
- pymetadataeditor-0.5.0/pymetadataeditor/projects.py +317 -0
- pymetadataeditor-0.5.0/pymetadataeditor/publish.py +203 -0
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/requester.py +100 -42
- pymetadataeditor-0.5.0/pymetadataeditor/resources.py +158 -0
- pymetadataeditor-0.5.0/pymetadataeditor/templates_client.py +49 -0
- pymetadataeditor-0.5.0/pymetadataeditor/tidy.py +197 -0
- pymetadataeditor-0.5.0/pymetadataeditor/uploads.py +92 -0
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pyproject.toml +8 -4
- pymetadataeditor-0.3.2/PKG-INFO +0 -327
- pymetadataeditor-0.3.2/README.md +0 -305
- pymetadataeditor-0.3.2/pymetadataeditor/__init__.py +0 -1
- pymetadataeditor-0.3.2/pymetadataeditor/interface.py +0 -1969
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/LICENSE +0 -0
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/templates.py +0 -0
- {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/utils.py +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymetadataeditor
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Metadata Editor client for python
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Gordon Blackadder
|
|
8
|
+
Author-email: gblackadder@worldbank.org
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
|
+
Provides-Extra: llm
|
|
18
|
+
Requires-Dist: markitdown (>=0.0.1a3,<0.0.2) ; extra == "llm"
|
|
19
|
+
Requires-Dist: metadataschemas (>=0.1.3,<0.2.0)
|
|
20
|
+
Requires-Dist: openai (>=2.8.0,<3.0.0) ; extra == "llm"
|
|
21
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
22
|
+
Requires-Dist: pydantic (>=2.7.4,<3.0.0)
|
|
23
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
24
|
+
Requires-Dist: tiktoken (>=0.8.0) ; extra == "llm"
|
|
25
|
+
Requires-Dist: tqdm ; extra == "llm"
|
|
26
|
+
Requires-Dist: urllib3 (>=2.2.2,<3.0.0)
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pyMetadataEditor
|
|
30
|
+
|
|
31
|
+
A Python client for [Metadata Editor](https://github.com/worldbank/metadata-editor): create, edit, and manage metadata for microdata, indicators, geospatial data, documents, scripts, images, and videos.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
This project uses [Poetry](https://python-poetry.org/docs/basic-usage/):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install poetry
|
|
39
|
+
cd pymetadataeditor
|
|
40
|
+
poetry install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The LLM-assisted metadata features (OpenAI, `tiktoken`, `markitdown`) are optional: `poetry install --extras llm`, or `pip install "pymetadataeditor[llm]"`.
|
|
44
|
+
|
|
45
|
+
For development (pytest, pre-commit):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
poetry install --with dev
|
|
49
|
+
poetry run pre-commit install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pymetadataeditor import MetadataEditor
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
me = MetadataEditor(
|
|
59
|
+
api_url=os.getenv("METADATA_API_URL"), # e.g. https://host/index.php/api
|
|
60
|
+
api_key=os.getenv("METADATA_API_KEY"),
|
|
61
|
+
verify_ssl=False, # set True when SSL is verified on your server
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### List projects
|
|
66
|
+
|
|
67
|
+
`list` returns one page (summary fields, no metadata) plus the server `total`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
response = me.projects.list(limit=10, metadata_type="indicator", keywords="literacy rate")
|
|
71
|
+
print(response["total"])
|
|
72
|
+
for project in response["projects"]:
|
|
73
|
+
print(project["id"], project["type"], project["title"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Filters: `keywords`, `metadata_type`, `collection` (+ `exclude_collections=True`), `tag`, `ownership`, `users`,
|
|
77
|
+
`created_by`, `changed_by`, `date_start` / `date_end` (last changed), `sort_by`. List filters accept Python lists.
|
|
78
|
+
`me.projects.facets()` returns the available types, collections and their counts.
|
|
79
|
+
|
|
80
|
+
To go through every matching project, iterate instead of loading everything at once:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
for project in me.projects.iter_all(metadata_type="indicator", page_size=500):
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
me.projects.count(collection=12) # just the number
|
|
87
|
+
me.projects.list_all(metadata_type="table") # everything in memory: {"projects": [...], "total": n}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`iter_all` pages in id order, so projects edited while you iterate are neither skipped nor repeated. Other large
|
|
91
|
+
listings have the same pattern: `me.collections.iter_projects`, `me.jobs.iter_all`, `me.codelists.iter_all`,
|
|
92
|
+
`me.codelists.iter_codes`, `me.data_structures.iter_all`, `me.indicators.iter_data_rows`.
|
|
93
|
+
|
|
94
|
+
### Create a project (template-aware)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
demo_name = "my_indicator_demo"
|
|
98
|
+
metadata = {
|
|
99
|
+
"idno": demo_name,
|
|
100
|
+
"series_description": {
|
|
101
|
+
"idno": demo_name,
|
|
102
|
+
"name": "Version 1",
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
project_id = me.metadata.create(
|
|
107
|
+
metadata,
|
|
108
|
+
metadata_type_or_template_uid="indicator",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Read metadata back
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
model = me.metadata.get(project_id, output_mode="pydantic")
|
|
116
|
+
model.pretty_print()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API overview (v2)
|
|
120
|
+
|
|
121
|
+
Thin clients return **raw API JSON** (`dict`). Workflow helpers on `me.metadata` handle validation and Pydantic/Excel conversion.
|
|
122
|
+
|
|
123
|
+
| Client | Role |
|
|
124
|
+
|--------|------|
|
|
125
|
+
| `me.projects` | List, get, create, update, patch, delete projects |
|
|
126
|
+
| `me.metadata` | Template-aware create, update, get (dict / pydantic / excel) |
|
|
127
|
+
| `me.collections` | Collections and membership |
|
|
128
|
+
| `me.templates` | Template CRUD |
|
|
129
|
+
| `me.resources` | External resources (reports, questionnaires, files) |
|
|
130
|
+
| `me.jobs` | Background jobs (publish to NADA, indicator data import, poll, list) |
|
|
131
|
+
| `me.publish` | Publish metadata and indicator DSD/data to NADA (synchronous) |
|
|
132
|
+
| `me.uploads` | Resumable (chunked) file uploads |
|
|
133
|
+
| `me.data_structures` | Data structure (DSD) registry: structures, components, CSV/JSON/SDMX import, export |
|
|
134
|
+
| `me.codelists` | Codelist registry: codelists, codes, translations, import/export |
|
|
135
|
+
| `me.indicators` | Indicator projects: bind a DSD, load/export data, chart data, `notes`, publish |
|
|
136
|
+
| `me.api` | Generic `get` / `post` / `put` / `delete` for any path |
|
|
137
|
+
|
|
138
|
+
API errors raise `ApiError` (`status_code`, `message`, `errors`), a subclass of `requests` `HTTPError`.
|
|
139
|
+
|
|
140
|
+
Methods still on `me` directly include `make_metadata_outline`, `read_metadata_from_excel`, `draft_metadata_from_files`, and related helpers.
|
|
141
|
+
|
|
142
|
+
**Breaking change:** v1 flat methods (`list_projects`, `create_project_log`, …) were removed. See [legacy-migration.MD](legacy-migration.MD).
|
|
143
|
+
|
|
144
|
+
### Direct HTTP (`me.api`)
|
|
145
|
+
|
|
146
|
+
For endpoints without a typed wrapper yet:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
me.api.get("/editor", params={"limit": 5})
|
|
150
|
+
me.api.post("/collections/add_projects", json={"collections": [1], "id_format": "id", "projects": [101]})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
POST body styles:
|
|
154
|
+
|
|
155
|
+
- `json={...}` — JSON API payloads
|
|
156
|
+
- `data={...}` — form fields
|
|
157
|
+
- `data={...}` + `files={...}` — multipart uploads
|
|
158
|
+
|
|
159
|
+
Many deletes use **POST** in restricted networks (e.g. `me.projects.delete`, `me.collections.delete`), not HTTP DELETE.
|
|
160
|
+
|
|
161
|
+
### Publish to NADA
|
|
162
|
+
|
|
163
|
+
`me.publish.*` publishes now and returns NADA's answers; `me.jobs.queue_*` methods queue a job on the
|
|
164
|
+
editor's worker and return its `uuid` (pass `wait=True` to block until it finishes). Both take the same steps:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
options = {"overwrite": "yes", "published": 1, "access_policy": "licensed", "repositoryid": "MY_COLLECTION"}
|
|
168
|
+
|
|
169
|
+
# now: {"results": {...}, "errors": {...}} per step
|
|
170
|
+
me.publish.project(123, catalog_connection_id=1, publish_thumbnail=False, options=options)
|
|
171
|
+
|
|
172
|
+
# background job
|
|
173
|
+
job = me.jobs.queue_publish_to_nada(123, catalog_connection_id=1, publish_thumbnail=False, options=options)
|
|
174
|
+
result = me.jobs.wait(job["uuid"], timeout_seconds=3600)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Single steps: `me.publish.metadata`, `indicator_data`, `thumbnail`, `resources`, `resource`, `delete_nada_resources`.
|
|
178
|
+
|
|
179
|
+
Step-by-step notebook: [`demo/publish_to_nada_jobs.ipynb`](demo/publish_to_nada_jobs.ipynb).
|
|
180
|
+
|
|
181
|
+
## Guides
|
|
182
|
+
|
|
183
|
+
- [Indicators](docs/indicators.md) — data structures, loading data, notes, publishing to NADA, pandas helpers
|
|
184
|
+
- [Metadata workflows](docs/metadata-workflows.md) — outlines, [Pydantic](docs/metadata-workflows.md#pydantic), [Excel](docs/metadata-workflows.md#excel), [retrieving metadata](docs/metadata-workflows.md#retrieving-existing-metadata)
|
|
185
|
+
- [LLM-assisted metadata](docs/llm-assisted-metadata.md) — draft and augment from documents and URLs
|
|
186
|
+
- [API examples](docs/examples.md) — collections, copying projects, and common tasks
|
|
187
|
+
- [Migrating from v1](legacy-migration.MD) — old → new method mapping
|
|
188
|
+
- [API reference](docs/API_Reference.md) — regenerate with `python make_docs.py`
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
### Poetry troubleshooting (Windows)
|
|
193
|
+
|
|
194
|
+
If NumPy or path-length errors occur:
|
|
195
|
+
|
|
196
|
+
1. Enable [long paths in Windows](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell#enable-long-paths-in-windows-10-version-1607-and-later), or
|
|
197
|
+
2. Run `poetry config virtualenvs.in-project true` then `poetry install`
|
|
198
|
+
|
|
199
|
+
### Tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
poetry run pytest tests/test_interface.py -q
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### API documentation
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python make_docs.py # demo notebooks -> docs/*.md, and docs/API_Reference.md
|
|
209
|
+
python make_docs.py --api-only # only docs/API_Reference.md
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Notes
|
|
213
|
+
|
|
214
|
+
Parts of this codebase were written with the assistance of ChatGPT, in keeping with World Bank Group practice.
|
|
215
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# pyMetadataEditor
|
|
2
|
+
|
|
3
|
+
A Python client for [Metadata Editor](https://github.com/worldbank/metadata-editor): create, edit, and manage metadata for microdata, indicators, geospatial data, documents, scripts, images, and videos.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This project uses [Poetry](https://python-poetry.org/docs/basic-usage/):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install poetry
|
|
11
|
+
cd pymetadataeditor
|
|
12
|
+
poetry install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The LLM-assisted metadata features (OpenAI, `tiktoken`, `markitdown`) are optional: `poetry install --extras llm`, or `pip install "pymetadataeditor[llm]"`.
|
|
16
|
+
|
|
17
|
+
For development (pytest, pre-commit):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
poetry install --with dev
|
|
21
|
+
poetry run pre-commit install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pymetadataeditor import MetadataEditor
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
me = MetadataEditor(
|
|
31
|
+
api_url=os.getenv("METADATA_API_URL"), # e.g. https://host/index.php/api
|
|
32
|
+
api_key=os.getenv("METADATA_API_KEY"),
|
|
33
|
+
verify_ssl=False, # set True when SSL is verified on your server
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### List projects
|
|
38
|
+
|
|
39
|
+
`list` returns one page (summary fields, no metadata) plus the server `total`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
response = me.projects.list(limit=10, metadata_type="indicator", keywords="literacy rate")
|
|
43
|
+
print(response["total"])
|
|
44
|
+
for project in response["projects"]:
|
|
45
|
+
print(project["id"], project["type"], project["title"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Filters: `keywords`, `metadata_type`, `collection` (+ `exclude_collections=True`), `tag`, `ownership`, `users`,
|
|
49
|
+
`created_by`, `changed_by`, `date_start` / `date_end` (last changed), `sort_by`. List filters accept Python lists.
|
|
50
|
+
`me.projects.facets()` returns the available types, collections and their counts.
|
|
51
|
+
|
|
52
|
+
To go through every matching project, iterate instead of loading everything at once:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
for project in me.projects.iter_all(metadata_type="indicator", page_size=500):
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
me.projects.count(collection=12) # just the number
|
|
59
|
+
me.projects.list_all(metadata_type="table") # everything in memory: {"projects": [...], "total": n}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`iter_all` pages in id order, so projects edited while you iterate are neither skipped nor repeated. Other large
|
|
63
|
+
listings have the same pattern: `me.collections.iter_projects`, `me.jobs.iter_all`, `me.codelists.iter_all`,
|
|
64
|
+
`me.codelists.iter_codes`, `me.data_structures.iter_all`, `me.indicators.iter_data_rows`.
|
|
65
|
+
|
|
66
|
+
### Create a project (template-aware)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
demo_name = "my_indicator_demo"
|
|
70
|
+
metadata = {
|
|
71
|
+
"idno": demo_name,
|
|
72
|
+
"series_description": {
|
|
73
|
+
"idno": demo_name,
|
|
74
|
+
"name": "Version 1",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
project_id = me.metadata.create(
|
|
79
|
+
metadata,
|
|
80
|
+
metadata_type_or_template_uid="indicator",
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Read metadata back
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
model = me.metadata.get(project_id, output_mode="pydantic")
|
|
88
|
+
model.pretty_print()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API overview (v2)
|
|
92
|
+
|
|
93
|
+
Thin clients return **raw API JSON** (`dict`). Workflow helpers on `me.metadata` handle validation and Pydantic/Excel conversion.
|
|
94
|
+
|
|
95
|
+
| Client | Role |
|
|
96
|
+
|--------|------|
|
|
97
|
+
| `me.projects` | List, get, create, update, patch, delete projects |
|
|
98
|
+
| `me.metadata` | Template-aware create, update, get (dict / pydantic / excel) |
|
|
99
|
+
| `me.collections` | Collections and membership |
|
|
100
|
+
| `me.templates` | Template CRUD |
|
|
101
|
+
| `me.resources` | External resources (reports, questionnaires, files) |
|
|
102
|
+
| `me.jobs` | Background jobs (publish to NADA, indicator data import, poll, list) |
|
|
103
|
+
| `me.publish` | Publish metadata and indicator DSD/data to NADA (synchronous) |
|
|
104
|
+
| `me.uploads` | Resumable (chunked) file uploads |
|
|
105
|
+
| `me.data_structures` | Data structure (DSD) registry: structures, components, CSV/JSON/SDMX import, export |
|
|
106
|
+
| `me.codelists` | Codelist registry: codelists, codes, translations, import/export |
|
|
107
|
+
| `me.indicators` | Indicator projects: bind a DSD, load/export data, chart data, `notes`, publish |
|
|
108
|
+
| `me.api` | Generic `get` / `post` / `put` / `delete` for any path |
|
|
109
|
+
|
|
110
|
+
API errors raise `ApiError` (`status_code`, `message`, `errors`), a subclass of `requests` `HTTPError`.
|
|
111
|
+
|
|
112
|
+
Methods still on `me` directly include `make_metadata_outline`, `read_metadata_from_excel`, `draft_metadata_from_files`, and related helpers.
|
|
113
|
+
|
|
114
|
+
**Breaking change:** v1 flat methods (`list_projects`, `create_project_log`, …) were removed. See [legacy-migration.MD](legacy-migration.MD).
|
|
115
|
+
|
|
116
|
+
### Direct HTTP (`me.api`)
|
|
117
|
+
|
|
118
|
+
For endpoints without a typed wrapper yet:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
me.api.get("/editor", params={"limit": 5})
|
|
122
|
+
me.api.post("/collections/add_projects", json={"collections": [1], "id_format": "id", "projects": [101]})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
POST body styles:
|
|
126
|
+
|
|
127
|
+
- `json={...}` — JSON API payloads
|
|
128
|
+
- `data={...}` — form fields
|
|
129
|
+
- `data={...}` + `files={...}` — multipart uploads
|
|
130
|
+
|
|
131
|
+
Many deletes use **POST** in restricted networks (e.g. `me.projects.delete`, `me.collections.delete`), not HTTP DELETE.
|
|
132
|
+
|
|
133
|
+
### Publish to NADA
|
|
134
|
+
|
|
135
|
+
`me.publish.*` publishes now and returns NADA's answers; `me.jobs.queue_*` methods queue a job on the
|
|
136
|
+
editor's worker and return its `uuid` (pass `wait=True` to block until it finishes). Both take the same steps:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
options = {"overwrite": "yes", "published": 1, "access_policy": "licensed", "repositoryid": "MY_COLLECTION"}
|
|
140
|
+
|
|
141
|
+
# now: {"results": {...}, "errors": {...}} per step
|
|
142
|
+
me.publish.project(123, catalog_connection_id=1, publish_thumbnail=False, options=options)
|
|
143
|
+
|
|
144
|
+
# background job
|
|
145
|
+
job = me.jobs.queue_publish_to_nada(123, catalog_connection_id=1, publish_thumbnail=False, options=options)
|
|
146
|
+
result = me.jobs.wait(job["uuid"], timeout_seconds=3600)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Single steps: `me.publish.metadata`, `indicator_data`, `thumbnail`, `resources`, `resource`, `delete_nada_resources`.
|
|
150
|
+
|
|
151
|
+
Step-by-step notebook: [`demo/publish_to_nada_jobs.ipynb`](demo/publish_to_nada_jobs.ipynb).
|
|
152
|
+
|
|
153
|
+
## Guides
|
|
154
|
+
|
|
155
|
+
- [Indicators](docs/indicators.md) — data structures, loading data, notes, publishing to NADA, pandas helpers
|
|
156
|
+
- [Metadata workflows](docs/metadata-workflows.md) — outlines, [Pydantic](docs/metadata-workflows.md#pydantic), [Excel](docs/metadata-workflows.md#excel), [retrieving metadata](docs/metadata-workflows.md#retrieving-existing-metadata)
|
|
157
|
+
- [LLM-assisted metadata](docs/llm-assisted-metadata.md) — draft and augment from documents and URLs
|
|
158
|
+
- [API examples](docs/examples.md) — collections, copying projects, and common tasks
|
|
159
|
+
- [Migrating from v1](legacy-migration.MD) — old → new method mapping
|
|
160
|
+
- [API reference](docs/API_Reference.md) — regenerate with `python make_docs.py`
|
|
161
|
+
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
### Poetry troubleshooting (Windows)
|
|
165
|
+
|
|
166
|
+
If NumPy or path-length errors occur:
|
|
167
|
+
|
|
168
|
+
1. Enable [long paths in Windows](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell#enable-long-paths-in-windows-10-version-1607-and-later), or
|
|
169
|
+
2. Run `poetry config virtualenvs.in-project true` then `poetry install`
|
|
170
|
+
|
|
171
|
+
### Tests
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
poetry run pytest tests/test_interface.py -q
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### API documentation
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python make_docs.py # demo notebooks -> docs/*.md, and docs/API_Reference.md
|
|
181
|
+
python make_docs.py --api-only # only docs/API_Reference.md
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Notes
|
|
185
|
+
|
|
186
|
+
Parts of this codebase were written with the assistance of ChatGPT, in keeping with World Bank Group practice.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .api_client import ApiClient
|
|
2
|
+
from .codelists import CodelistsClient
|
|
3
|
+
from .collections import CollectionsClient
|
|
4
|
+
from .data_structures import DataStructuresClient, components_from_csv
|
|
5
|
+
from .exceptions import ApiError, DeleteNotAppliedError, TemplateError
|
|
6
|
+
from .indicators import IndicatorNotesClient, IndicatorsClient
|
|
7
|
+
from .interface import MetadataEditor
|
|
8
|
+
from .jobs import JobsClient
|
|
9
|
+
from .metadata_workflows import MetadataWorkflowsClient
|
|
10
|
+
from .projects import ProjectsClient
|
|
11
|
+
from .publish import PublishClient
|
|
12
|
+
from .resources import ResourcesClient
|
|
13
|
+
from .templates_client import TemplatesClient
|
|
14
|
+
from .uploads import UploadsClient
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MetadataEditor",
|
|
18
|
+
"ApiClient",
|
|
19
|
+
"ProjectsClient",
|
|
20
|
+
"CollectionsClient",
|
|
21
|
+
"TemplatesClient",
|
|
22
|
+
"ResourcesClient",
|
|
23
|
+
"PublishClient",
|
|
24
|
+
"JobsClient",
|
|
25
|
+
"UploadsClient",
|
|
26
|
+
"DataStructuresClient",
|
|
27
|
+
"CodelistsClient",
|
|
28
|
+
"IndicatorsClient",
|
|
29
|
+
"IndicatorNotesClient",
|
|
30
|
+
"components_from_csv",
|
|
31
|
+
"MetadataWorkflowsClient",
|
|
32
|
+
"ApiError",
|
|
33
|
+
"DeleteNotAppliedError",
|
|
34
|
+
"TemplateError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Generic HTTP verb wrappers for calling any Metadata Editor API path."""
|
|
2
|
+
|
|
3
|
+
from io import BufferedReader
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .requester import RequestsWithSpecificErrors
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiClient:
|
|
10
|
+
"""Call API endpoints directly when no typed sub-client method exists yet."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, api: RequestsWithSpecificErrors):
|
|
13
|
+
self._api = api
|
|
14
|
+
|
|
15
|
+
def request(
|
|
16
|
+
self,
|
|
17
|
+
method: str,
|
|
18
|
+
path: str,
|
|
19
|
+
id: Optional[Union[int, str]] = None,
|
|
20
|
+
params: Optional[Dict[str, Union[str, List[str]]]] = None,
|
|
21
|
+
json: Optional[Dict] = None,
|
|
22
|
+
data: Optional[Dict[str, str]] = None,
|
|
23
|
+
files: Optional[Dict[str, BufferedReader]] = None,
|
|
24
|
+
) -> Dict:
|
|
25
|
+
"""Issue an HTTP request against a path relative to the configured API base URL."""
|
|
26
|
+
return self._api.request(
|
|
27
|
+
method=method,
|
|
28
|
+
pth=path,
|
|
29
|
+
id=id,
|
|
30
|
+
params=params,
|
|
31
|
+
json=json,
|
|
32
|
+
data=data,
|
|
33
|
+
files=files,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get(
|
|
37
|
+
self,
|
|
38
|
+
path: str,
|
|
39
|
+
id: Optional[Union[int, str]] = None,
|
|
40
|
+
params: Optional[Dict[str, Union[str, List[str]]]] = None,
|
|
41
|
+
) -> Dict:
|
|
42
|
+
"""GET request."""
|
|
43
|
+
return self.request("get", path, id=id, params=params)
|
|
44
|
+
|
|
45
|
+
def post(
|
|
46
|
+
self,
|
|
47
|
+
path: str,
|
|
48
|
+
id: Optional[Union[int, str]] = None,
|
|
49
|
+
json: Optional[Dict] = None,
|
|
50
|
+
data: Optional[Dict[str, str]] = None,
|
|
51
|
+
files: Optional[Dict[str, BufferedReader]] = None,
|
|
52
|
+
params: Optional[Dict[str, Union[str, List[str]]]] = None,
|
|
53
|
+
) -> Dict:
|
|
54
|
+
"""POST request. Prefer POST delete routes when DELETE is blocked by your network."""
|
|
55
|
+
return self.request("post", path, id=id, params=params, json=json, data=data, files=files)
|
|
56
|
+
|
|
57
|
+
def put(
|
|
58
|
+
self,
|
|
59
|
+
path: str,
|
|
60
|
+
id: Optional[Union[int, str]] = None,
|
|
61
|
+
json: Optional[Dict] = None,
|
|
62
|
+
data: Optional[Dict[str, str]] = None,
|
|
63
|
+
files: Optional[Dict[str, BufferedReader]] = None,
|
|
64
|
+
params: Optional[Dict[str, Union[str, List[str]]]] = None,
|
|
65
|
+
) -> Dict:
|
|
66
|
+
"""PUT request."""
|
|
67
|
+
return self.request("put", path, id=id, params=params, json=json, data=data, files=files)
|
|
68
|
+
|
|
69
|
+
def delete(
|
|
70
|
+
self,
|
|
71
|
+
path: str,
|
|
72
|
+
id: Optional[Union[int, str]] = None,
|
|
73
|
+
params: Optional[Dict[str, Union[str, List[str]]]] = None,
|
|
74
|
+
) -> Dict:
|
|
75
|
+
"""DELETE request. Many editor deletes use POST instead (e.g. `/editor/delete/{{id}}`)."""
|
|
76
|
+
return self.request("delete", path, id=id, params=params)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared argument types and project type normalization for Metadata Editor API paths."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Union
|
|
4
|
+
|
|
5
|
+
#: A project argument: the numeric project id (e.g. ``1234``) or the project idno (e.g. ``"ALB_2012_LSMS"``).
|
|
6
|
+
#: IDNOs cannot be numeric only, so a numeric value is always the project id.
|
|
7
|
+
ProjectIdOrIdno = Union[int, str]
|
|
8
|
+
|
|
9
|
+
# Legacy / display names -> OpenAPI path segment (hyphenated).
|
|
10
|
+
_PROJECT_TYPE_ALIASES: Dict[str, str] = {
|
|
11
|
+
"indicator": "timeseries",
|
|
12
|
+
"indicators_db": "timeseries-db",
|
|
13
|
+
"microdata": "survey",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_project_type(project_type: str) -> str:
|
|
18
|
+
"""Map legacy or display type names to OpenAPI project-type path segments."""
|
|
19
|
+
key = project_type.strip().lower().replace("-", "_")
|
|
20
|
+
mapped = _PROJECT_TYPE_ALIASES.get(key, key)
|
|
21
|
+
return mapped.replace("_", "-")
|