Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Implement sync client without event loop #89

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
repos:
- repo: https://github.com/ambv/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.259
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
name: sort imports with ruff
args: [--select, I, --fix]
- id: ruff-format
name: format with ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.981
hooks:
Expand Down
72 changes: 38 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,57 +48,61 @@ Async version:

```python
import asyncio
from rossum_api import ElisAPIClient
from rossum_api import AsyncRossumAPIClient

WORKSPACE = {
"name": "Rossum Client NG Test",
"organization": "https://elis.rossum.ai/api/v1/organizations/116390",
"name": "Rossum Client NG Test",
"organization": "https://elis.rossum.ai/api/v1/organizations/116390",
}


async def main_with_async_client():
client = ElisAPIClient(
os.environ["ELIS_USERNAME"],
os.environ["ELIS_PASSWORD"],
base_url="https://elis.rossum.ai/api/v1",
)
ws = await client.create_new_workspace(data=WORKSPACE)
workspace_id = ws.id
ws = await client.retrieve_workspace(workspace_id)
print("GET result:", ws)
print("LIST results:")
async for w in client.list_all_workspaces(ordering=["-id"], name=WORKSPACE["name"]):
print(w)
await client.delete_workspace(workspace_id)
print(f"Workspace {workspace_id} deleted.")
client = AsyncRossumAPIClient(
os.environ["ELIS_USERNAME"],
os.environ["ELIS_PASSWORD"],
base_url="https://elis.rossum.ai/api/v1",
)
ws = await client.create_new_workspace(data=WORKSPACE)
workspace_id = ws.id
ws = await client.retrieve_workspace(workspace_id)
print("GET result:", ws)
print("LIST results:")
async for w in client.list_all_workspaces(ordering=["-id"], name=WORKSPACE["name"]):
print(w)
await client.delete_workspace(workspace_id)
print(f"Workspace {workspace_id} deleted.")


asyncio.run(main_with_async_client())
```

Sync version:

```python
from rossum_api import ElisAPIClientSync
from rossum_api import SyncRossumAPIClient

WORKSPACE = {
"name": "Rossum Client NG Test",
"organization": "https://elis.rossum.ai/api/v1/organizations/116390",
"name": "Rossum Client NG Test",
"organization": "https://elis.rossum.ai/api/v1/organizations/116390",
}


def main_with_sync_client():
client = ElisAPIClientSync(
os.environ["ELIS_USERNAME"],
os.environ["ELIS_PASSWORD"],
base_url="https://elis.rossum.ai/api/v1",
)
ws = client.create_new_workspace(data=WORKSPACE)
workspace_id = ws.id
ws = client.retrieve_workspace(workspace_id)
print("GET result:", ws)
print("LIST results:")
for w in client.list_all_workspaces(ordering=["-id"], name=WORKSPACE["name"]):
print(w)
client.delete_workspace(workspace_id)
print(f"Workspace {workspace_id} deleted.")
client = SyncRossumAPIClient(
os.environ["ELIS_USERNAME"],
os.environ["ELIS_PASSWORD"],
base_url="https://elis.rossum.ai/api/v1",
)
ws = client.create_new_workspace(data=WORKSPACE)
workspace_id = ws.id
ws = client.retrieve_workspace(workspace_id)
print("GET result:", ws)
print("LIST results:")
for w in client.list_all_workspaces(ordering=["-id"], name=WORKSPACE["name"]):
print(w)
client.delete_workspace(workspace_id)
print(f"Workspace {workspace_id} deleted.")


main_with_sync_client()
```
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ requires-python = ">= 3.8"
dependencies = [
"aiofiles",
"dacite",
"inflect",
"httpx",
"tenacity",
]
Expand All @@ -24,6 +25,7 @@ tests = [
"pytest-cov",
"ruff",
"types-aiofiles",
"pre-commit",
]

[tools.setuptools]
Expand Down
10 changes: 5 additions & 5 deletions rossum_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from __future__ import annotations

from rossum_api.api_client import APIClientError
from rossum_api.elis_api_client import ElisAPIClient, ExportFileFormats
from rossum_api.elis_api_client_sync import ElisAPIClientSync
from rossum_api.clients.external_async_client import AsyncRossumAPIClient, ExportFileFormats
from rossum_api.clients.external_sync_client import SyncRossumAPIClient
from rossum_api.clients.internal_async_client import APIClientError

__version__ = "0.20.0"

__all__ = (
"APIClientError",
"ElisAPIClient",
"ElisAPIClientSync",
"AsyncRossumAPIClient",
"SyncRossumAPIClient",
"ExportFileFormats",
)
Empty file added rossum_api/clients/__init__.py
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@

import aiofiles

from rossum_api.api_client import APIClient
from rossum_api.clients.internal_async_client import InternalAsyncClient
from rossum_api.domain_logic.annotations import (
is_annotation_imported,
validate_list_annotations_params,
)
from rossum_api.domain_logic.documents import build_create_document_params
from rossum_api.domain_logic.resources import Resource
from rossum_api.domain_logic.urls import DEFAULT_BASE_URL
from rossum_api.domain_logic.search import build_search_params, validate_search_params
from rossum_api.domain_logic.urls import DEFAULT_BASE_URL, parse_resource_id_from_url
from rossum_api.models import deserialize_default
from rossum_api.models.task import TaskStatus

Expand Down Expand Up @@ -47,14 +53,14 @@ class Sideload:
pass


class ElisAPIClient:
class AsyncRossumAPIClient:
def __init__(
self,
username: Optional[str] = None,
password: Optional[str] = None,
token: Optional[str] = None,
base_url: str = DEFAULT_BASE_URL,
http_client: Optional[APIClient] = None,
http_client: Optional[InternalAsyncClient] = None,
deserializer: Optional[Deserializer] = None,
):
"""
Expand All @@ -66,7 +72,7 @@ def __init__(
deserializer
pass a custom deserialization callable if different model classes should be returned
"""
self._http_client = http_client or APIClient(username, password, token, base_url)
self._http_client = http_client or InternalAsyncClient(base_url, username, password, token)
self._deserializer = deserializer or deserialize_default

# ##### QUEUE #####
Expand Down Expand Up @@ -141,7 +147,7 @@ async def _upload(self, file, queue_id, filename, values, metadata) -> int:
Resource.Queue, queue_id, fp, filename, values, metadata
)
(result,) = results["results"] # We're uploading 1 file in 1 request, we can unpack
return int(result["annotation"].split("/")[-1])
return parse_resource_id_from_url(result["annotation"])

# ##### UPLOAD #####
async def upload_document(
Expand Down Expand Up @@ -200,7 +206,7 @@ async def _create_upload(
files["metadata"] = ("", json.dumps(metadata).encode("utf-8"), "application/json")

task_url = await self.request_json("POST", url, files=files)
task_id = task_url["url"].split("/")[-1]
task_id = parse_resource_id_from_url(task_url["url"])

return await self.retrieve_task(task_id)

Expand Down Expand Up @@ -253,7 +259,7 @@ async def retrieve_organization(self, org_id: int) -> Organization:
async def retrieve_own_organization(self) -> Organization:
"""Retrieve organization of currently logged in user."""
user: Dict[Any, Any] = await self._http_client.fetch_one(Resource.Auth, "user")
organization_id = user["organization"].split("/")[-1]
organization_id = parse_resource_id_from_url(user["organization"])
return await self.retrieve_organization(organization_id)

# ##### SCHEMAS #####
Expand Down Expand Up @@ -321,10 +327,7 @@ async def list_all_annotations(
**filters: Any,
) -> AsyncIterator[Annotation]:
"""https://elis.rossum.ai/api/docs/#list-all-annotations."""
if sideloads and "content" in sideloads and not content_schema_ids:
raise ValueError(
'When content sideloading is requested, "content_schema_ids" must be provided'
)
validate_list_annotations_params(sideloads, content_schema_ids)
async for a in self._http_client.fetch_all(
Resource.Annotation, ordering, sideloads, content_schema_ids, **filters
):
Expand All @@ -339,13 +342,8 @@ async def search_for_annotations(
**kwargs: Any,
) -> AsyncIterator[Annotation]:
"""https://elis.rossum.ai/api/docs/#search-for-annotations."""
if not query and not query_string:
raise ValueError("Either query or query_string must be provided")
json_payload = {}
if query:
json_payload["query"] = query
if query_string:
json_payload["query_string"] = query_string
validate_search_params(query, query_string)
json_payload = build_search_params(query, query_string)

async for a in self._http_client.fetch_all_by_url(
f"{Resource.Annotation.value}/search",
Expand Down Expand Up @@ -394,9 +392,7 @@ async def poll_annotation_until_imported(
self, annotation_id: int, **poll_kwargs: Any
) -> Annotation:
"""A shortcut for waiting until annotation is imported."""
return await self.poll_annotation(
annotation_id, lambda a: a.status not in ("importing", "created"), **poll_kwargs
)
return await self.poll_annotation(annotation_id, is_annotation_imported, **poll_kwargs)

async def poll_task(
self,
Expand Down Expand Up @@ -514,13 +510,7 @@ async def create_new_document(
parent: Optional[str] = None,
) -> Document:
"""https://elis.rossum.ai/api/docs/#create-document"""
metadata = metadata or {}
files: httpx._types.RequestFiles = {
"content": (file_name, file_data),
"metadata": ("", json.dumps(metadata).encode("utf-8")),
}
if parent:
files["parent"] = ("", parent)
files = build_create_document_params(file_name, file_data, metadata, parent)

document = await self._http_client.request_json(
"POST", url=Resource.Document.value, files=files
Expand Down Expand Up @@ -573,7 +563,7 @@ async def list_all_email_templates(
self,
ordering: Sequence[str] = (),
**filters: Any,
) -> AsyncIterator[Connector]:
) -> AsyncIterator[EmailTemplate]:
"""https://elis.rossum.ai/api/docs/#list-all-email-templates."""
async for c in self._http_client.fetch_all(Resource.EmailTemplate, ordering, **filters):
yield self._deserializer(Resource.EmailTemplate, c)
Expand Down
Loading