Skip to content

Commit

Permalink
add: entity_list methods create, add_property, get; plus example script
Browse files Browse the repository at this point in the history
  • Loading branch information
lindsay-stevens committed May 23, 2024
1 parent 8bd94ff commit 349a8ed
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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,
)
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
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.
: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)
32 changes: 31 additions & 1 deletion tests/endpoints/test_entity_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
31 changes: 31 additions & 0 deletions tests/resources/entity_lists_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
},
],
},
]
24 changes: 23 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Loading

0 comments on commit 349a8ed

Please sign in to comment.