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.
Files changed (31) hide show
  1. pymetadataeditor-0.5.0/PKG-INFO +215 -0
  2. pymetadataeditor-0.5.0/README.md +186 -0
  3. pymetadataeditor-0.5.0/pymetadataeditor/__init__.py +35 -0
  4. pymetadataeditor-0.5.0/pymetadataeditor/api_client.py +76 -0
  5. pymetadataeditor-0.5.0/pymetadataeditor/api_types.py +21 -0
  6. pymetadataeditor-0.5.0/pymetadataeditor/codelists.py +229 -0
  7. pymetadataeditor-0.5.0/pymetadataeditor/collections.py +154 -0
  8. pymetadataeditor-0.5.0/pymetadataeditor/constants.py +5 -0
  9. pymetadataeditor-0.5.0/pymetadataeditor/data_structures.py +353 -0
  10. pymetadataeditor-0.5.0/pymetadataeditor/exceptions.py +90 -0
  11. pymetadataeditor-0.5.0/pymetadataeditor/indicators.py +454 -0
  12. pymetadataeditor-0.5.0/pymetadataeditor/interface.py +999 -0
  13. pymetadataeditor-0.5.0/pymetadataeditor/jobs.py +291 -0
  14. {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/llm_helpers.py +3 -3
  15. pymetadataeditor-0.5.0/pymetadataeditor/metadata_workflows.py +229 -0
  16. pymetadataeditor-0.5.0/pymetadataeditor/paging.py +75 -0
  17. pymetadataeditor-0.5.0/pymetadataeditor/projects.py +317 -0
  18. pymetadataeditor-0.5.0/pymetadataeditor/publish.py +203 -0
  19. {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/requester.py +100 -42
  20. pymetadataeditor-0.5.0/pymetadataeditor/resources.py +158 -0
  21. pymetadataeditor-0.5.0/pymetadataeditor/templates_client.py +49 -0
  22. pymetadataeditor-0.5.0/pymetadataeditor/tidy.py +197 -0
  23. pymetadataeditor-0.5.0/pymetadataeditor/uploads.py +92 -0
  24. {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pyproject.toml +8 -4
  25. pymetadataeditor-0.3.2/PKG-INFO +0 -327
  26. pymetadataeditor-0.3.2/README.md +0 -305
  27. pymetadataeditor-0.3.2/pymetadataeditor/__init__.py +0 -1
  28. pymetadataeditor-0.3.2/pymetadataeditor/interface.py +0 -1969
  29. {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/LICENSE +0 -0
  30. {pymetadataeditor-0.3.2 → pymetadataeditor-0.5.0}/pymetadataeditor/templates.py +0 -0
  31. {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("_", "-")