From 237a8ca646f9dcf86dc7641399c0665d1f9c0729 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 23 May 2024 21:28:33 +1000 Subject: [PATCH 1/6] chg: remove pyodk examples requirements.txt files - probably fair to assume that someone using these examples has the current pyodk release installed. Otherwise, as the number of example scripts grows it would become a bit of a chore to update them all, especially if the target release version isn't yet known. --- docs/examples/app_user_provisioner/requirements.txt | 1 - docs/examples/create_or_update_form/requirements.txt | 1 - docs/examples/mail_merge/requirements.txt | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 docs/examples/create_or_update_form/requirements.txt diff --git a/docs/examples/app_user_provisioner/requirements.txt b/docs/examples/app_user_provisioner/requirements.txt index 04015f4..b9f7537 100644 --- a/docs/examples/app_user_provisioner/requirements.txt +++ b/docs/examples/app_user_provisioner/requirements.txt @@ -1,3 +1,2 @@ -pyodk==0.3.0 segno==1.6.1 Pillow==10.3.0 diff --git a/docs/examples/create_or_update_form/requirements.txt b/docs/examples/create_or_update_form/requirements.txt deleted file mode 100644 index 9485a46..0000000 --- a/docs/examples/create_or_update_form/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pyodk==1.0.0 diff --git a/docs/examples/mail_merge/requirements.txt b/docs/examples/mail_merge/requirements.txt index c677b85..628d29f 100644 --- a/docs/examples/mail_merge/requirements.txt +++ b/docs/examples/mail_merge/requirements.txt @@ -1,2 +1 @@ -pyodk==0.3.0 -docx-mailmerge2=0.8.0 \ No newline at end of file +docx-mailmerge2==0.8.0 From d26f6e3cdfd6c7fab4add2a14b97668f1a20ee7c Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 23 May 2024 21:30:20 +1000 Subject: [PATCH 2/6] chg: update usages of wrap_error / str_validator to use validate_str - the changed lines were written before the validate_str wrapper was added. --- pyodk/_endpoints/comments.py | 4 +--- pyodk/_endpoints/project_app_users.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pyodk/_endpoints/comments.py b/pyodk/_endpoints/comments.py index c27b65c..adada4a 100644 --- a/pyodk/_endpoints/comments.py +++ b/pyodk/_endpoints/comments.py @@ -96,9 +96,7 @@ def post( pid = pv.validate_project_id(project_id, self.default_project_id) fid = pv.validate_form_id(form_id, self.default_form_id) iid = pv.validate_instance_id(instance_id, self.default_instance_id) - comment = pv.wrap_error( - validator=pv.v.str_validator, key="comment", value=comment - ) + comment = pv.validate_str(comment, key="comment") json = {"body": comment} except PyODKError as err: log.error(err, exc_info=True) diff --git a/pyodk/_endpoints/project_app_users.py b/pyodk/_endpoints/project_app_users.py index 8429423..025bbed 100644 --- a/pyodk/_endpoints/project_app_users.py +++ b/pyodk/_endpoints/project_app_users.py @@ -81,9 +81,7 @@ def create( """ try: pid = pv.validate_project_id(project_id, self.default_project_id) - display_name = pv.wrap_error( - validator=pv.v.str_validator, key="display_name", value=display_name - ) + display_name = pv.validate_str(display_name, key="display_name") json = {"displayName": display_name} except PyODKError as err: log.error(err, exc_info=True) From 07725263f1b2633a98bdc4432871484781b7999f Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 23 May 2024 21:33:10 +1000 Subject: [PATCH 3/6] fix: validators wrap_error not catching all pydantic errors - in the last deps update, pydantic changed errors using ValidationError as a base to use PydanticTypeError or PydanticValueError. - added tests to catch this sort of thing in future - added optional "key" param to validate_file_path so that all funcs have more consistent signatures --- pyodk/_utils/validators.py | 9 ++++----- tests/test_validators.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/test_validators.py diff --git a/pyodk/_utils/validators.py b/pyodk/_utils/validators.py index cf1b9e6..77ad52c 100644 --- a/pyodk/_utils/validators.py +++ b/pyodk/_utils/validators.py @@ -4,8 +4,7 @@ from typing import Any from pydantic.v1 import validators as v -from pydantic.v1.errors import PydanticValueError -from pydantic_core._pydantic_core import ValidationError +from pydantic.v1.errors import PydanticTypeError, PydanticValueError from pyodk._utils.utils import coalesce from pyodk.errors import PyODKError @@ -22,7 +21,7 @@ def wrap_error(validator: Callable, key: str, value: Any) -> Any: """ try: return validator(value) - except (ValidationError, PydanticValueError) as err: + except (PydanticTypeError, PydanticValueError) as err: msg = f"{key}: {err!s}" raise PyODKError(msg) from err @@ -99,9 +98,9 @@ def validate_dict(*args: dict, key: str) -> int: ) -def validate_file_path(*args: PathLike | str) -> Path: +def validate_file_path(*args: PathLike | str, key: str = "file_path") -> Path: def validate_fp(f): p = v.path_validator(f) return v.path_exists_validator(p) - return wrap_error(validator=validate_fp, key="file_path", value=coalesce(*args)) + return wrap_error(validator=validate_fp, key=key, value=coalesce(*args)) diff --git a/tests/test_validators.py b/tests/test_validators.py new file mode 100644 index 0000000..45ff5b1 --- /dev/null +++ b/tests/test_validators.py @@ -0,0 +1,41 @@ +from unittest import TestCase + +from pyodk._utils import validators as v +from pyodk.errors import PyODKError + + +class TestValidators(TestCase): + def test_wrap_error__raises_pyodk_error(self): + """Should raise a PyODK error (from Pydantic) if a validator check fails.""" + + def a_func(): + pass + + cases = ( + (v.validate_project_id, False, (None, "a")), + (v.validate_form_id, False, (None, a_func)), + (v.validate_table_name, False, (None, a_func)), + (v.validate_instance_id, False, (None, a_func)), + (v.validate_entity_list_name, False, (None, a_func)), + (v.validate_str, True, (None, a_func)), + (v.validate_bool, True, (None, a_func)), + (v.validate_int, True, (None, a_func)), + (v.validate_dict, True, (None, ((("a",),),))), + ( + v.validate_file_path, + True, + ( + None, + "No such file", + ), + ), + ) + + for i, (func, has_key, values) in enumerate(cases): + for j, value in enumerate(values): + msg = f"Case {i}, Value {j}" + with self.subTest(msg=msg), self.assertRaises(PyODKError): + if has_key: + func(value, key=msg) + else: + func(value) From 8bd94ff79d8daa9b2e0da333fb1ddcbdf5363fe9 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 23 May 2024 21:37:47 +1000 Subject: [PATCH 4/6] add: convenience func to PyODKError for detecting Central HTTP error - makes it easier to do a "create if not exists" / EAFP instead of LBYL. --- .../create_or_update_form.py | 23 ++++++++----------- pyodk/errors.py | 16 +++++++++++++ tests/utils/forms.py | 9 ++------ tests/utils/submissions.py | 4 +++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/docs/examples/create_or_update_form/create_or_update_form.py b/docs/examples/create_or_update_form/create_or_update_form.py index dbda43a..1b8e0c5 100644 --- a/docs/examples/create_or_update_form/create_or_update_form.py +++ b/docs/examples/create_or_update_form/create_or_update_form.py @@ -1,11 +1,3 @@ -import sys -from os import PathLike -from pathlib import Path - -from pyodk.client import Client -from pyodk.errors import PyODKError -from requests import Response - """ A script to create or update a form, optionally with attachments. @@ -14,18 +6,21 @@ If provided, all files in the [attachments_dir] path will be uploaded with the form. """ +import sys +from os import PathLike +from pathlib import Path + +from pyodk.client import Client +from pyodk.errors import PyODKError + def create_ignore_duplicate_error(client: Client, definition: PathLike | str | bytes): """Create the form; ignore the error raised if it exists (409.3).""" try: client.forms.create(definition=definition) except PyODKError as err: - if len(err.args) >= 2 and isinstance(err.args[1], Response): - err_detail = err.args[1].json() - err_code = err_detail.get("code") - if err_code is not None and str(err_code) == "409.3": - return - raise + if not err.is_central_error(code=409.3): + raise def create_or_update(form_id: str, definition: str, attachments: str | None): diff --git a/pyodk/errors.py b/pyodk/errors.py index 7b43487..f40fb37 100644 --- a/pyodk/errors.py +++ b/pyodk/errors.py @@ -1,2 +1,18 @@ +from requests import Response + + class PyODKError(Exception): """An error raised by pyodk.""" + + def is_central_error(self, code: float | str) -> bool: + """ + Does the PyODK error represent a Central error with the specified code? + + Per central-backend/lib/util/problem.js. + """ + if len(self.args) >= 2 and isinstance(self.args[1], Response): + err_detail = self.args[1].json() + err_code = str(err_detail.get("code", "")) + if err_code is not None and err_code == str(code): + return True + return False diff --git a/tests/utils/forms.py b/tests/utils/forms.py index 9df3e1a..941c3ac 100644 --- a/tests/utils/forms.py +++ b/tests/utils/forms.py @@ -2,7 +2,6 @@ from pyodk.client import Client from pyodk.errors import PyODKError -from requests import Response from tests.utils import utils from tests.utils.md_table import md_table_to_temp_dir @@ -17,12 +16,8 @@ def create_ignore_duplicate_error( try: client.forms.create(definition=definition, form_id=form_id) except PyODKError as err: - if len(err.args) >= 2 and isinstance(err.args[1], Response): - err_detail = err.args[1].json() - err_code = err_detail.get("code") - if err_code is not None and str(err_code) == "409.3": - return - raise + if not err.is_central_error(code=409.3): + raise def create_new_form__md(client: Client, form_id: str, form_def: str): diff --git a/tests/utils/submissions.py b/tests/utils/submissions.py index 371505f..1faeaf3 100644 --- a/tests/utils/submissions.py +++ b/tests/utils/submissions.py @@ -27,7 +27,9 @@ def create_new_or_get_last_submission( ), form_id=form_id, ).instanceId - except PyODKError: + except PyODKError as err: + if not err.is_central_error(code=409.3): + raise subvs = client.session.get( client.session.urlformat( "projects/{pid}/forms/{fid}/submissions/{iid}/versions", From 349a8ed72f88b00dd5d3f471a3be6f3831452d5d Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 23 May 2024 21:40:57 +1000 Subject: [PATCH 5/6] add: entity_list methods create, add_property, get; plus example script --- .../create_entities_from_submissions.py | 30 +++++ .../imported_answers.csv | 4 + pyodk/_endpoints/entity_list_properties.py | 82 ++++++++++++ pyodk/_endpoints/entity_lists.py | 125 +++++++++++++++++- tests/endpoints/test_entity_lists.py | 32 ++++- tests/resources/entity_lists_data.py | 31 +++++ tests/test_client.py | 24 +++- tests/utils/entity_lists.py | 36 ++--- 8 files changed, 333 insertions(+), 31 deletions(-) create mode 100644 docs/examples/create_entities_from_submissions/create_entities_from_submissions.py create mode 100644 docs/examples/create_entities_from_submissions/imported_answers.csv create mode 100644 pyodk/_endpoints/entity_list_properties.py diff --git a/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py b/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py new file mode 100644 index 0000000..2bab25d --- /dev/null +++ b/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py @@ -0,0 +1,30 @@ +""" +A script that uses CSV data create an entity list and populate it with entities. +""" + +import csv +from pathlib import Path +from uuid import uuid4 + +from pyodk import Client + +if __name__ == "__main__": + project_id = 1 + entity_list_name = f"previous_survey_{uuid4()}" + entity_label_field = "first_name" + entity_properties = ("age", "location") + csv_path = Path("./imported_answers.csv") + + with Client(project_id=project_id) as client, open(csv_path) as csv_file: + # Create the entity list. + client.entity_lists.create(entity_list_name=entity_list_name) + for prop in entity_properties: + client.entity_lists.add_property(name=prop, entity_list_name=entity_list_name) + + # Create the entities from the CSV data. + for row in csv.DictReader(csv_file): + client.entities.create( + label=row[entity_label_field], + data={k: str(v) for k, v in row.items() if k in entity_properties}, + entity_list_name=entity_list_name, + ) diff --git a/docs/examples/create_entities_from_submissions/imported_answers.csv b/docs/examples/create_entities_from_submissions/imported_answers.csv new file mode 100644 index 0000000..935ddcd --- /dev/null +++ b/docs/examples/create_entities_from_submissions/imported_answers.csv @@ -0,0 +1,4 @@ +first_name,age,favorite_color,favorite_color_other,location +John,30,r,,37.7749 -122.4194 0 10 +Alice,25,y,,-33.8651 151.2099 0 5 +Bob,35,o,orange,51.5074 -0.1278 0 15 \ No newline at end of file diff --git a/pyodk/_endpoints/entity_list_properties.py b/pyodk/_endpoints/entity_list_properties.py new file mode 100644 index 0000000..09032fc --- /dev/null +++ b/pyodk/_endpoints/entity_list_properties.py @@ -0,0 +1,82 @@ +import logging +from datetime import datetime + +from pyodk._endpoints import bases +from pyodk._utils import validators as pv +from pyodk._utils.session import Session +from pyodk.errors import PyODKError + +log = logging.getLogger(__name__) + + +class EntityListProperty(bases.Model): + name: str + odataName: str + publishedAt: datetime + forms: list[str] + + +class URLs(bases.Model): + class Config: + frozen = True + + post: str = "projects/{project_id}/datasets/{entity_list_name}/properties" + + +class EntityListPropertyService(bases.Service): + __slots__ = ( + "urls", + "session", + "default_project_id", + "default_entity_list_name", + ) + + def __init__( + self, + session: Session, + default_project_id: int | None = None, + default_entity_list_name: str | None = None, + urls: URLs = None, + ): + self.urls: URLs = urls if urls is not None else URLs() + self.session: Session = session + self.default_project_id: int | None = default_project_id + self.default_entity_list_name: str | None = default_entity_list_name + + def create( + self, + name: str, + entity_list_name: str | None = None, + project_id: int | None = None, + ) -> bool: + """ + Create an Entity List Property. + + :param name: The name of the Property. Property names follow the same rules as + form field names (valid XML identifiers) and cannot use the reserved names of + name or label, or begin with the reserved prefix __. + :param entity_list_name: The name of the Entity List (Dataset) being referenced. + :param project_id: The id of the project this Entity List belongs to. + """ + try: + pid = pv.validate_project_id(project_id, self.default_project_id) + eln = pv.validate_entity_list_name( + entity_list_name, self.default_entity_list_name + ) + req_data = {"name": pv.validate_str(name, key="name")} + except PyODKError as err: + log.error(err, exc_info=True) + raise + + response = self.session.response_or_error( + method="POST", + url=self.session.urlformat( + self.urls.post, + project_id=pid, + entity_list_name=eln, + ), + logger=log, + json=req_data, + ) + data = response.json() + return data["success"] diff --git a/pyodk/_endpoints/entity_lists.py b/pyodk/_endpoints/entity_lists.py index bf0f9bb..5ae6c52 100644 --- a/pyodk/_endpoints/entity_lists.py +++ b/pyodk/_endpoints/entity_lists.py @@ -1,7 +1,12 @@ import logging from datetime import datetime +from typing import Any from pyodk._endpoints import bases +from pyodk._endpoints.entity_list_properties import ( + EntityListProperty, + EntityListPropertyService, +) from pyodk._utils import validators as pv from pyodk._utils.session import Session from pyodk.errors import PyODKError @@ -14,13 +19,17 @@ class EntityList(bases.Model): projectId: int createdAt: datetime approvalRequired: bool + properties: list[EntityListProperty] | None = None class URLs(bases.Model): class Config: frozen = True - list: str = "projects/{project_id}/datasets" + _entity_list = "projects/{project_id}/datasets" + list: str = _entity_list + post: str = _entity_list + get: str = f"{_entity_list}/{{entity_list_name}}" class EntityListService(bases.Service): @@ -40,21 +49,59 @@ class EntityListService(bases.Service): multiple EntityLists. """ - __slots__ = ("urls", "session", "default_project_id") + __slots__ = ( + "urls", + "session", + "_default_project_id", + "_default_entity_list_name", + "_property_service", + "add_property", + ) def __init__( self, session: Session, default_project_id: int | None = None, + default_entity_list_name: str | None = None, urls: URLs = None, ): self.urls: URLs = urls if urls is not None else URLs() self.session: Session = session - self.default_project_id: int | None = default_project_id + self._property_service = EntityListPropertyService(session=self.session) + self.add_property = self._property_service.create + + self._default_project_id: int | None = None + self.default_project_id = default_project_id + self._default_entity_list_name: str | None = None + self.default_entity_list_name = default_entity_list_name + + def _default_kw(self) -> dict[str, Any]: + return { + "default_project_id": self.default_project_id, + "default_entity_list_name": self.default_entity_list_name, + } + + @property + def default_project_id(self) -> int | None: + return self._default_project_id + + @default_project_id.setter + def default_project_id(self, v) -> None: + self._default_project_id = v + self._property_service.default_project_id = v + + @property + def default_entity_list_name(self) -> str | None: + return self._default_entity_list_name + + @default_entity_list_name.setter + def default_entity_list_name(self, v) -> None: + self._default_entity_list_name = v + self._property_service.default_entity_list_name = v def list(self, project_id: int | None = None) -> list[EntityList]: """ - Read Entity List details. + Read all Entity List details. :param project_id: The id of the project the Entity List belongs to. @@ -73,3 +120,73 @@ def list(self, project_id: int | None = None) -> list[EntityList]: ) data = response.json() return [EntityList(**r) for r in data] + + def get( + self, + entity_list_name: str | None = None, + project_id: int | None = None, + ) -> EntityList: + """ + Read Entity List details. + + :param project_id: The id of the project the Entity List belongs to. + :param entity_list_name: The name of the Entity List (Dataset) being referenced. + + :return: An object representation of all Entity Lists' details. + """ + try: + pid = pv.validate_project_id(project_id, self.default_project_id) + eln = pv.validate_entity_list_name( + entity_list_name, self.default_entity_list_name + ) + except PyODKError as err: + log.error(err, exc_info=True) + raise + + response = self.session.response_or_error( + method="GET", + url=self.session.urlformat( + self.urls.get, project_id=pid, entity_list_name=eln + ), + logger=log, + ) + data = response.json() + return EntityList(**data) + + def create( + self, + approval_required: bool | None = False, + entity_list_name: str | None = None, + project_id: int | None = None, + ) -> EntityList: + """ + Create an Entity. + + :param approval_required: If False, create Entities as soon as Submissions are + received by Central. If True, create Entities when Submissions are marked as + Approved in Central. + :param entity_list_name: The name of the Entity List (Dataset) being referenced. + :param project_id: The id of the project this Entity List belongs to. + """ + try: + pid = pv.validate_project_id(project_id, self.default_project_id) + req_data = { + "name": pv.validate_entity_list_name( + entity_list_name, self.default_entity_list_name + ), + "approvalRequired": pv.validate_bool( + approval_required, key="approval_required" + ), + } + except PyODKError as err: + log.error(err, exc_info=True) + raise + + response = self.session.response_or_error( + method="POST", + url=self.session.urlformat(self.urls.post, project_id=pid), + logger=log, + json=req_data, + ) + data = response.json() + return EntityList(**data) diff --git a/tests/endpoints/test_entity_lists.py b/tests/endpoints/test_entity_lists.py index 517c121..9ca1e90 100644 --- a/tests/endpoints/test_entity_lists.py +++ b/tests/endpoints/test_entity_lists.py @@ -19,7 +19,37 @@ def test_list__ok(self): mock_session.return_value.json.return_value = fixture with Client() as client: observed = client.entity_lists.list() - self.assertEqual(2, len(observed)) + self.assertEqual(3, len(observed)) for i, o in enumerate(observed): with self.subTest(i): self.assertIsInstance(o, EntityList) + + def test_get__ok(self): + """Should an EntityList object.""" + fixture = entity_lists_data.test_entity_lists[2] + with patch.object(Session, "request") as mock_session: + mock_session.return_value.status_code = 200 + mock_session.return_value.json.return_value = fixture + with Client() as client: + observed = client.entity_lists.get(entity_list_name="pyodk_test_eln") + self.assertIsInstance(observed, EntityList) + + def test_create__ok(self): + """Should return an EntityList object.""" + fixture = entity_lists_data.test_entity_lists + with patch.object(Session, "request") as mock_session: + mock_session.return_value.status_code = 200 + mock_session.return_value.json.return_value = fixture[0] + with Client() as client: + # Specify project + observed = client.entity_lists.create( + project_id=2, + entity_list_name="test", + approval_required=False, + ) + self.assertIsInstance(observed, EntityList) + # Use default + client.entity_lists.default_entity_list_name = "test" + client.entity_lists.default_project_id = 2 + observed = client.entity_lists.create() + self.assertIsInstance(observed, EntityList) diff --git a/tests/resources/entity_lists_data.py b/tests/resources/entity_lists_data.py index 9f2a227..2a5703a 100644 --- a/tests/resources/entity_lists_data.py +++ b/tests/resources/entity_lists_data.py @@ -11,4 +11,35 @@ "projectId": 1, "approvalRequired": False, }, + { + "name": "pyodk_test_eln", + "createdAt": "2024-04-17T13:16:32.960Z", + "projectId": 1, + "approvalRequired": False, + "entities": 5, + "lastEntity": "2024-05-22T05:52:02.868Z", + "conflicts": 0, + "linkedForms": [], + "sourceForms": [], + "properties": [ + { + "name": "test_label", + "publishedAt": "2024-04-17T13:16:33.172Z", + "odataName": "test_label", + "forms": [], + }, + { + "name": "another_prop", + "publishedAt": "2024-04-17T13:16:33.383Z", + "odataName": "another_prop", + "forms": [], + }, + { + "name": "third_property", + "publishedAt": "2024-05-22T05:46:29.578Z", + "odataName": "third_property", + "forms": [], + }, + ], + }, ] diff --git a/tests/test_client.py b/tests/test_client.py index 0876fd5..45e7629 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -286,6 +286,28 @@ def test_entities__update(self): self.assertEqual("test_value3", forced.currentVersion.data["test_label"]) def test_entity_lists__list(self): - """Should return a list of entities""" + """Should return a list of Entity Lists.""" observed = self.client.entity_lists.list() self.assertGreater(len(observed), 0) + + def test_entity_lists__create_and_query(self): + """Should create a new Entity List, and query it afterwards via list().""" + self.client.entity_lists.default_entity_list_name = ( + self.client.session.get_xform_uuid() + ) + entity_list = self.client.entity_lists.create() + entity_lists = self.client.entity_lists.list() + self.assertIn( + (entity_list.name, entity_list.projectId), + [(e.name, e.projectId) for e in entity_lists], + ) + + def test_entity_lists__add_property(self): + """Should create a new property on the Entity List.""" + self.client.entity_lists.default_entity_list_name = ( + self.client.session.get_xform_uuid() + ) + self.client.entity_lists.create() + self.client.entity_lists.add_property(name="test") + entity_list = self.client.entity_lists.get() + self.assertEqual(["test"], [p.name for p in entity_list.properties]) diff --git a/tests/utils/entity_lists.py b/tests/utils/entity_lists.py index d6039b1..b50e507 100644 --- a/tests/utils/entity_lists.py +++ b/tests/utils/entity_lists.py @@ -1,4 +1,4 @@ -from pyodk._endpoints.entity_lists import EntityList, log +from pyodk._endpoints.entity_lists import EntityList from pyodk.client import Client from pyodk.errors import PyODKError @@ -14,16 +14,10 @@ def create_new_or_get_entity_list( :param entity_props: Properties to add to the entity list. """ try: - entity_list = client.session.response_or_error( - method="POST", - url=client.session.urlformat( - "projects/{pid}/datasets", - pid=client.project_id, - ), - logger=log, - json={"name": entity_list_name}, - ) - except PyODKError: + entity_list = client.entity_lists.create(entity_list_name=entity_list_name) + except PyODKError as err: + if not err.is_central_error(code=409.3): + raise entity_list = client.session.get( url=client.session.urlformat( "projects/{pid}/datasets/{eln}", @@ -31,18 +25,10 @@ def create_new_or_get_entity_list( eln=entity_list_name, ), ) - try: - for prop in entity_props: - client.session.response_or_error( - method="GET", - url=client.session.urlformat( - "projects/{pid}/datasets/{eln}/properties", - pid=client.project_id, - eln=entity_list_name, - ), - logger=log, - json={"name": prop}, - ) - except PyODKError: - pass + for prop in entity_props: + try: + client.entity_lists.add_property(name=prop, entity_list_name=entity_list_name) + except PyODKError as err: + if not err.is_central_error(code=409.3): + raise return EntityList(**entity_list.json()) From 3444dc069975bca5cd7480b75dda4c5e87160d7c Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 4 Jun 2024 03:06:11 +1000 Subject: [PATCH 6/6] fix: typos --- .../create_entities_from_submissions.py | 2 +- pyodk/_endpoints/entity_lists.py | 2 +- tests/endpoints/test_entity_lists.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py b/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py index 2bab25d..6421576 100644 --- a/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py +++ b/docs/examples/create_entities_from_submissions/create_entities_from_submissions.py @@ -1,5 +1,5 @@ """ -A script that uses CSV data create an entity list and populate it with entities. +A script that uses CSV data to create an entity list and populate it with entities. """ import csv diff --git a/pyodk/_endpoints/entity_lists.py b/pyodk/_endpoints/entity_lists.py index 5ae6c52..cb94f36 100644 --- a/pyodk/_endpoints/entity_lists.py +++ b/pyodk/_endpoints/entity_lists.py @@ -160,7 +160,7 @@ def create( project_id: int | None = None, ) -> EntityList: """ - Create an Entity. + Create an Entity List. :param approval_required: If False, create Entities as soon as Submissions are received by Central. If True, create Entities when Submissions are marked as diff --git a/tests/endpoints/test_entity_lists.py b/tests/endpoints/test_entity_lists.py index 9ca1e90..96ef818 100644 --- a/tests/endpoints/test_entity_lists.py +++ b/tests/endpoints/test_entity_lists.py @@ -25,7 +25,7 @@ def test_list__ok(self): self.assertIsInstance(o, EntityList) def test_get__ok(self): - """Should an EntityList object.""" + """Should return an EntityList object.""" fixture = entity_lists_data.test_entity_lists[2] with patch.object(Session, "request") as mock_session: mock_session.return_value.status_code = 200