Skip to content

Commit

Permalink
Merge pull request #87 from lindsay-stevens/pyodk-74
Browse files Browse the repository at this point in the history
74: add entity_list create and add_property
  • Loading branch information
lindsay-stevens authored Jun 3, 2024
2 parents 3c29896 + 3444dc0 commit fc58f7b
Show file tree
Hide file tree
Showing 19 changed files with 411 additions and 68 deletions.
1 change: 0 additions & 1 deletion docs/examples/app_user_provisioner/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pyodk==0.3.0
segno==1.6.1
Pillow==10.3.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
A script that uses CSV data to 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,
)
Original file line number Diff line number Diff line change
@@ -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
23 changes: 9 additions & 14 deletions docs/examples/create_or_update_form/create_or_update_form.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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):
Expand Down
1 change: 0 additions & 1 deletion docs/examples/create_or_update_form/requirements.txt

This file was deleted.

3 changes: 1 addition & 2 deletions docs/examples/mail_merge/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pyodk==0.3.0
docx-mailmerge2=0.8.0
docx-mailmerge2==0.8.0
4 changes: 1 addition & 3 deletions pyodk/_endpoints/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
82 changes: 82 additions & 0 deletions pyodk/_endpoints/entity_list_properties.py
Original file line number Diff line number Diff line change
@@ -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"]
125 changes: 121 additions & 4 deletions pyodk/_endpoints/entity_lists.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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 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
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)
4 changes: 1 addition & 3 deletions pyodk/_endpoints/project_app_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 4 additions & 5 deletions pyodk/_utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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))
Loading

0 comments on commit fc58f7b

Please sign in to comment.