Skip to content

Commit 1673cc0

Browse files
authored
Merge pull request #87 from rossumai/oh/domain-logic-part-1
chore: Extract Resource & URL domain logic into a reusable module
2 parents ee66d37 + 826fcd5 commit 1673cc0

24 files changed

+156
-54
lines changed

rossum_api/api_client.py

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,31 @@
66
import json
77
import logging
88
import typing
9-
from enum import Enum
109

1110
import httpx
1211
import tenacity
1312

13+
from rossum_api.domain_logic.urls import (
14+
DEFAULT_BASE_URL,
15+
build_export_url,
16+
build_full_login_url,
17+
build_upload_url,
18+
parse_annotation_id_from_datapoint_url,
19+
parse_resource_id_from_url,
20+
)
21+
1422
if typing.TYPE_CHECKING:
1523
from typing import Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple, Union
1624

1725
from aiofiles.threadpool.binary import AsyncBufferedReader
1826

27+
from rossum_api.domain_logic.resources import Resource
28+
1929

2030
RETRIED_HTTP_CODES = (408, 429, 500, 502, 503, 504)
2131
logger = logging.getLogger(__name__)
2232

2333

24-
class Resource(Enum):
25-
"""Convenient representation of resources provided by Elis API.
26-
27-
Value is always the corresponding URL part.
28-
"""
29-
30-
Annotation = "annotations"
31-
Auth = "auth"
32-
Connector = "connectors"
33-
Document = "documents"
34-
EmailTemplate = "email_templates"
35-
Group = "groups"
36-
Hook = "hooks"
37-
Inbox = "inboxes"
38-
Organization = "organizations"
39-
Queue = "queues"
40-
Schema = "schemas"
41-
Task = "tasks"
42-
Upload = "uploads"
43-
User = "users"
44-
Workspace = "workspaces"
45-
Engine = "engines"
46-
47-
4834
class APIClientError(Exception):
4935
def __init__(self, status_code, error):
5036
self.status_code = status_code
@@ -123,7 +109,7 @@ def __init__(
123109
username: Optional[str] = None,
124110
password: Optional[str] = None,
125111
token: Optional[str] = None,
126-
base_url: Optional[str] = "https://elis.rossum.ai/api/v1",
112+
base_url: str = DEFAULT_BASE_URL,
127113
timeout: Optional[float] = None,
128114
n_retries: int = 3,
129115
retry_backoff_factor: float = 1.0,
@@ -295,9 +281,7 @@ def _embed_sideloads(
295281
# Datapoints from all annotations are present in content, we have to construct
296282
# content (list of datapoints) for each annotation
297283
def annotation_id(datapoint):
298-
return int(
299-
datapoint["url"].replace(f"/content/{datapoint['id']}", "").split("/")[-1]
300-
)
284+
return parse_annotation_id_from_datapoint_url(datapoint["url"])
301285

302286
sideloads_by_id[sideload_group] = {
303287
k: list(v)
@@ -315,7 +299,7 @@ def annotation_id(datapoint):
315299
url = result[sideload_name]
316300
if url is None:
317301
continue
318-
sideload_id = int(url.replace("/content", "").split("/")[-1])
302+
sideload_id = parse_resource_id_from_url(url)
319303

320304
result[sideload_name] = sideloads_by_id[sideload_group].get(
321305
sideload_id, []
@@ -361,7 +345,6 @@ async def upload(
361345
may be used to initialize values of the object created from the uploaded file,
362346
semantics is different for each resource
363347
"""
364-
url = f"{resource.value}/{id_}/upload"
365348
files = {"content": (filename, await fp.read(), "application/octet-stream")}
366349

367350
# Filename of values and metadata must be "", otherwise Elis API returns HTTP 400 with body
@@ -370,7 +353,7 @@ async def upload(
370353
files["values"] = ("", json.dumps(values).encode("utf-8"), "application/json")
371354
if metadata is not None:
372355
files["metadata"] = ("", json.dumps(metadata).encode("utf-8"), "application/json")
373-
return await self.request_json("POST", url, files=files)
356+
return await self.request_json("POST", build_upload_url(resource, id_), files=files)
374357

375358
async def export(
376359
self,
@@ -386,7 +369,7 @@ async def export(
386369
query_params = {**query_params, **filters}
387370
if columns:
388371
query_params["columns"] = ",".join(columns)
389-
url = f"{resource.value}/{id_}/export"
372+
url = build_export_url(resource, id_)
390373
# to_status parameter is valid only in POST requests, we can use GET in all other cases
391374
method = "POST" if "to_status" in filters else "GET"
392375
if export_format == "json":
@@ -426,7 +409,7 @@ async def _authenticate(self) -> None:
426409
async for attempt in self._retrying():
427410
with attempt:
428411
response = await self.client.post(
429-
f"{self.base_url}/auth/login",
412+
build_full_login_url(self.base_url),
430413
data={"username": self.username, "password": self.password},
431414
)
432415
await self._raise_for_status(response)

rossum_api/domain_logic/__init__.py

Whitespace-only changes.

rossum_api/domain_logic/resources.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
5+
6+
class Resource(Enum):
7+
"""Convenient representation of resources provided by Elis API.
8+
9+
Value is always the corresponding URL part.
10+
"""
11+
12+
Annotation = "annotations"
13+
Auth = "auth"
14+
Connector = "connectors"
15+
Document = "documents"
16+
EmailTemplate = "email_templates"
17+
Group = "groups"
18+
Hook = "hooks"
19+
Inbox = "inboxes"
20+
Organization = "organizations"
21+
Queue = "queues"
22+
Schema = "schemas"
23+
Task = "tasks"
24+
Upload = "uploads"
25+
User = "users"
26+
Workspace = "workspaces"
27+
Engine = "engines"

rossum_api/domain_logic/urls.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from rossum_api.models import Resource
8+
9+
DEFAULT_BASE_URL = "https://elis.rossum.ai/api/v1"
10+
11+
12+
def parse_resource_id_from_url(url: str) -> int:
13+
# Annotation content resource is special, we need to strip /content suffix
14+
return int(url.replace("/content", "").split("/")[-1])
15+
16+
17+
def parse_annotation_id_from_datapoint_url(url: str) -> int:
18+
# URL format: .../annotation/<annotation ID>/content/<datapoint ID>
19+
# Remove the /content/<datapoint ID> from the URL and then pass it to the generic function.
20+
return parse_resource_id_from_url(re.sub(r"/content/.*", "", url))
21+
22+
23+
def build_url(resource: Resource, id_: int) -> str:
24+
return f"{resource.value}/{id_}"
25+
26+
27+
def build_export_url(resource: Resource, id_: int) -> str:
28+
return f"{build_url(resource, id_)}/export"
29+
30+
31+
def build_upload_url(resource: Resource, id_: int) -> str:
32+
return f"{build_url(resource, id_)}/upload"
33+
34+
35+
def build_full_login_url(base_url: str) -> str:
36+
return f"{base_url}/auth/login"

rossum_api/elis_api_client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
import aiofiles
99

10-
from rossum_api.api_client import APIClient, Resource
10+
from rossum_api.api_client import APIClient
11+
from rossum_api.domain_logic.resources import Resource
12+
from rossum_api.domain_logic.urls import DEFAULT_BASE_URL
1113
from rossum_api.models import deserialize_default
1214
from rossum_api.models.task import TaskStatus
1315

@@ -51,7 +53,7 @@ def __init__(
5153
username: Optional[str] = None,
5254
password: Optional[str] = None,
5355
token: Optional[str] = None,
54-
base_url: Optional[str] = None,
56+
base_url: str = DEFAULT_BASE_URL,
5557
http_client: Optional[APIClient] = None,
5658
deserializer: Optional[Deserializer] = None,
5759
):

rossum_api/elis_api_client_sync.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from queue import Queue as ThreadSafeQueue
77

88
from rossum_api import ElisAPIClient
9+
from rossum_api.domain_logic.urls import DEFAULT_BASE_URL
910

1011
if typing.TYPE_CHECKING:
1112
import pathlib
@@ -61,7 +62,7 @@ def __init__(
6162
username: Optional[str] = None,
6263
password: Optional[str] = None,
6364
token: Optional[str] = None,
64-
base_url: Optional[str] = None,
65+
base_url: str = DEFAULT_BASE_URL,
6566
http_client: Optional[APIClient] = None,
6667
deserializer: Optional[Deserializer] = None,
6768
):

rossum_api/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import dacite
77

8-
from rossum_api.api_client import Resource
8+
from rossum_api.domain_logic.resources import Resource
99
from rossum_api.models.annotation import Annotation
1010
from rossum_api.models.connector import Connector
1111
from rossum_api.models.document import Document

tests/domain_logic/test_urls.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from rossum_api.domain_logic.resources import Resource
6+
from rossum_api.domain_logic.urls import (
7+
build_export_url,
8+
build_full_login_url,
9+
build_upload_url,
10+
build_url,
11+
parse_annotation_id_from_datapoint_url,
12+
parse_resource_id_from_url,
13+
)
14+
15+
16+
@pytest.mark.parametrize(
17+
"url, expected_id",
18+
[
19+
("https://elis.rossum.ai/api/v1/queues/8199", 8199),
20+
("https://elis.rossum.ai/api/v1/annotations/314521/content", 314521),
21+
],
22+
)
23+
def test_parse_resource_id_from_url(url, expected_id):
24+
assert parse_resource_id_from_url(url) == expected_id
25+
26+
27+
def test_parse_annotation_id_from_datapoint_url():
28+
assert (
29+
parse_annotation_id_from_datapoint_url(
30+
"https://elis.rossum.ai/api/v1/annotations/314521/content/1123123"
31+
)
32+
== 314521
33+
)
34+
35+
36+
def test_build_url():
37+
assert build_url(Resource.Queue, 123) == "queues/123"
38+
39+
40+
def test_build_full_login_url():
41+
assert (
42+
build_full_login_url("https://elis.rossum.ai/api/v1")
43+
== "https://elis.rossum.ai/api/v1/auth/login"
44+
)
45+
46+
47+
def test_build_upload_url():
48+
assert build_upload_url(Resource.Queue, 123) == "queues/123/upload"
49+
50+
51+
def test_build_export_url():
52+
assert build_export_url(Resource.Queue, 123) == "queues/123/export"

tests/e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from aiofiles import os as aios
2020

2121
from rossum_api import ElisAPIClient
22-
from rossum_api.api_client import Resource
22+
from rossum_api.domain_logic.resources import Resource
2323

2424
if TYPE_CHECKING:
2525
from typing import Optional

tests/elis_api_client/test_annotations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from rossum_api.api_client import Resource
7+
from rossum_api.domain_logic.resources import Resource
88
from rossum_api.models.annotation import Annotation
99
from rossum_api.models.automation_blocker import AutomationBlocker, AutomationBlockerContent
1010
from rossum_api.models.document import Document

tests/elis_api_client/test_connectors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.connector import Connector
77

88

tests/elis_api_client/test_documents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import httpx
66
import pytest
77

8-
from rossum_api.api_client import Resource
8+
from rossum_api.domain_logic.resources import Resource
99
from rossum_api.models.document import Document
1010

1111

tests/elis_api_client/test_email_templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.email_template import EmailTemplate
77

88

tests/elis_api_client/test_groups.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.group import Group
77

88

tests/elis_api_client/test_hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.hook import Hook
77

88

tests/elis_api_client/test_inboxes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.inbox import Inbox
77

88

tests/elis_api_client/test_organizations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from rossum_api.api_client import Resource
7+
from rossum_api.domain_logic.resources import Resource
88
from rossum_api.models.organization import Organization
99

1010

tests/elis_api_client/test_queues.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from rossum_api.api_client import Resource
7+
from rossum_api.domain_logic.resources import Resource
88
from rossum_api.models.annotation import Annotation
99
from rossum_api.models.queue import Queue
1010
from rossum_api.models.task import Task, TaskStatus, TaskType

tests/elis_api_client/test_schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from rossum_api.api_client import Resource
5+
from rossum_api.domain_logic.resources import Resource
66
from rossum_api.models.schema import Schema
77

88

tests/elis_api_client/test_tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from rossum_api.api_client import Resource
7+
from rossum_api.domain_logic.resources import Resource
88
from rossum_api.models.task import Task, TaskStatus
99

1010

0 commit comments

Comments
 (0)