aidbox-python-sdk 0.1.7__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {aidbox-python-sdk-0.1.7/aidbox_python_sdk.egg-info → aidbox_python_sdk-0.1.9}/PKG-INFO +17 -7
  2. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/README.md +2 -4
  3. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/__init__.py +2 -2
  4. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/aidboxpy.py +16 -17
  5. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/db.py +26 -35
  6. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/exceptions.py +1 -1
  7. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/handlers.py +8 -10
  8. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/main.py +9 -18
  9. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/pytest_plugin.py +12 -15
  10. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/sdk.py +15 -24
  11. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/settings.py +4 -4
  12. aidbox_python_sdk-0.1.9/aidbox_python_sdk/types.py +16 -0
  13. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9/aidbox_python_sdk.egg-info}/PKG-INFO +17 -7
  14. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk.egg-info/SOURCES.txt +1 -1
  15. aidbox_python_sdk-0.1.9/aidbox_python_sdk.egg-info/requires.txt +16 -0
  16. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/pyproject.toml +24 -14
  17. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/tests/test_sdk.py +8 -8
  18. aidbox-python-sdk-0.1.7/aidbox_python_sdk/gunicorn.py +0 -20
  19. aidbox-python-sdk-0.1.7/aidbox_python_sdk.egg-info/requires.txt +0 -19
  20. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/LICENSE.md +0 -0
  21. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/MANIFEST.in +0 -0
  22. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk/db_migrations.py +0 -0
  23. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk.egg-info/dependency_links.txt +0 -0
  24. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk.egg-info/not-zip-safe +0 -0
  25. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/aidbox_python_sdk.egg-info/top_level.txt +0 -0
  26. {aidbox-python-sdk-0.1.7 → aidbox_python_sdk-0.1.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aidbox-python-sdk
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Aidbox SDK for python
5
5
  Author-email: "beda.software" <aidbox-python-sdk@beda.software>
6
6
  License: MIT License
@@ -35,33 +35,43 @@ Classifier: Intended Audience :: Developers
35
35
  Classifier: Operating System :: OS Independent
36
36
  Classifier: Programming Language :: Python
37
37
  Classifier: Programming Language :: Python :: 3
38
- Classifier: Programming Language :: Python :: 3.8
39
38
  Classifier: Programming Language :: Python :: 3.9
40
39
  Classifier: Programming Language :: Python :: 3.10
41
40
  Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
42
  Classifier: Topic :: Internet :: WWW/HTTP
43
43
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
44
  Requires-Python: >=3.8
45
45
  Description-Content-Type: text/markdown
46
+ License-File: LICENSE.md
47
+ Requires-Dist: aiohttp>=3.6.2
48
+ Requires-Dist: SQLAlchemy>=1.3.10
49
+ Requires-Dist: fhirpy>=1.3.0
50
+ Requires-Dist: jsonschema>=4.4.0
46
51
  Provides-Extra: test
52
+ Requires-Dist: pytest~=6.0.0; extra == "test"
53
+ Requires-Dist: pytest-asyncio~=0.20.0; extra == "test"
54
+ Requires-Dist: pytest-aiohttp~=1.0.4; extra == "test"
55
+ Requires-Dist: pytest-cov~=4.0.0; extra == "test"
47
56
  Provides-Extra: dev
48
- License-File: LICENSE.md
57
+ Requires-Dist: black; extra == "dev"
58
+ Requires-Dist: autohooks; extra == "dev"
59
+ Requires-Dist: autohooks-plugin-ruff; extra == "dev"
60
+ Requires-Dist: autohooks-plugin-black; extra == "dev"
49
61
 
50
62
  [![build status](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml/badge.svg)](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml)
51
63
  [![pypi](https://img.shields.io/pypi/v/aidbox-python-sdk.svg)](https://pypi.org/project/aidbox-python-sdk/)
52
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
64
+ [![Supported Python version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/release/python-390/)
53
65
 
54
66
  # aidbox-python-sdk
55
67
 
56
- 1. Create a python 3.8+ environment `pyenv `
68
+ 1. Create a python 3.9+ environment `pyenv `
57
69
  2. Set env variables and activate virtual environment `source activate_settings.sh`
58
70
  2. Install the required packages with `pipenv install --dev`
59
71
  3. Make sure the app's settings are configured correctly (see `activate_settings.sh` and `aidbox_python_sdk/settings.py`). You can also
60
72
  use environment variables to define sensitive settings, eg. DB connection variables (see example `.env-ptl`)
61
73
  4. You can then run example with `python example.py`.
62
74
 
63
- Add `APP_FAST_START_MODE=TRUE` to env_tests for fast start mode.
64
-
65
75
  # Getting started
66
76
  ## Minimal application
67
77
  ```Python
@@ -1,18 +1,16 @@
1
1
  [![build status](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml/badge.svg)](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml)
2
2
  [![pypi](https://img.shields.io/pypi/v/aidbox-python-sdk.svg)](https://pypi.org/project/aidbox-python-sdk/)
3
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
3
+ [![Supported Python version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/release/python-390/)
4
4
 
5
5
  # aidbox-python-sdk
6
6
 
7
- 1. Create a python 3.8+ environment `pyenv `
7
+ 1. Create a python 3.9+ environment `pyenv `
8
8
  2. Set env variables and activate virtual environment `source activate_settings.sh`
9
9
  2. Install the required packages with `pipenv install --dev`
10
10
  3. Make sure the app's settings are configured correctly (see `activate_settings.sh` and `aidbox_python_sdk/settings.py`). You can also
11
11
  use environment variables to define sensitive settings, eg. DB connection variables (see example `.env-ptl`)
12
12
  4. You can then run example with `python example.py`.
13
13
 
14
- Add `APP_FAST_START_MODE=TRUE` to env_tests for fast start mode.
15
-
16
14
  # Getting started
17
15
  ## Minimal application
18
16
  ```Python
@@ -1,8 +1,8 @@
1
1
  __title__ = "aidbox-python-sdk"
2
- __version__ = "0.1.7"
2
+ __version__ = "0.1.9"
3
3
  __author__ = "beda.software"
4
4
  __license__ = "None"
5
- __copyright__ = "Copyright 2023 beda.software"
5
+ __copyright__ = "Copyright 2024 beda.software"
6
6
 
7
7
  # Version synonym
8
8
  VERSION = __version__
@@ -1,16 +1,17 @@
1
+ # type: ignore because fhir-py is not typed properly
1
2
  from abc import ABC
2
3
 
3
4
  from fhirpy.base import (
4
- SyncClient,
5
5
  AsyncClient,
6
- SyncSearchSet,
7
- AsyncSearchSet,
8
- SyncResource,
6
+ AsyncReference,
9
7
  AsyncResource,
8
+ AsyncSearchSet,
9
+ SyncClient,
10
10
  SyncReference,
11
- AsyncReference,
11
+ SyncResource,
12
+ SyncSearchSet,
12
13
  )
13
- from fhirpy.base.resource import BaseResource, BaseReference
14
+ from fhirpy.base.resource import BaseReference, BaseResource
14
15
  from fhirpy.base.searchset import AbstractSearchSet
15
16
 
16
17
  __title__ = "aidbox-py"
@@ -76,13 +77,14 @@ class BaseAidboxReference(BaseReference, ABC):
76
77
  Returns reference if local resource is saved
77
78
  """
78
79
  if self.is_local:
79
- return "{0}/{1}".format(self.resource_type, self.id)
80
+ return f"{self.resource_type}/{self.id}"
80
81
  return self.get("url", None)
81
82
 
82
83
  @property
83
84
  def id(self):
84
85
  if self.is_local:
85
86
  return self.get("id", None)
87
+ return None
86
88
 
87
89
  @property
88
90
  def resource_type(self):
@@ -91,6 +93,7 @@ class BaseAidboxReference(BaseReference, ABC):
91
93
  """
92
94
  if self.is_local:
93
95
  return self.get("resourceType", None)
96
+ return None
94
97
 
95
98
  @property
96
99
  def is_local(self):
@@ -109,16 +112,14 @@ class SyncAidboxClient(SyncClient):
109
112
  searchset_class = SyncAidboxSearchSet
110
113
  resource_class = SyncAidboxResource
111
114
 
112
- def reference(self, resource_type=None, id=None, reference=None, **kwargs):
115
+ def reference(self, resource_type=None, id=None, reference=None, **kwargs): # noqa: A002
113
116
  resource_type = kwargs.pop("resourceType", resource_type)
114
117
  if reference:
115
118
  if reference.count("/") > 1:
116
119
  return SyncAidboxReference(self, url=reference, **kwargs)
117
- resource_type, id = reference.split("/")
120
+ resource_type, id = reference.split("/") # noqa: A001
118
121
  if not resource_type and not id:
119
- raise TypeError(
120
- "Arguments `resource_type` and `id` or `reference`" "are required"
121
- )
122
+ raise TypeError("Arguments `resource_type` and `id` or `reference`are required")
122
123
  return SyncAidboxReference(self, resourceType=resource_type, id=id, **kwargs)
123
124
 
124
125
 
@@ -126,14 +127,12 @@ class AsyncAidboxClient(AsyncClient):
126
127
  searchset_class = AsyncAidboxSearchSet
127
128
  resource_class = AsyncAidboxResource
128
129
 
129
- def reference(self, resource_type=None, id=None, reference=None, **kwargs):
130
+ def reference(self, resource_type=None, id=None, reference=None, **kwargs): # noqa: A002
130
131
  resource_type = kwargs.pop("resourceType", resource_type)
131
132
  if reference:
132
133
  if reference.count("/") > 1:
133
134
  return AsyncAidboxReference(self, url=reference, **kwargs)
134
- resource_type, id = reference.split("/")
135
+ resource_type, id = reference.split("/") # noqa: A001
135
136
  if not resource_type and not id:
136
- raise TypeError(
137
- "Arguments `resource_type` and `id` or `reference`" "are required"
138
- )
137
+ raise TypeError("Arguments `resource_type` and `id` or `reference`are required")
139
138
  return AsyncAidboxReference(self, resourceType=resource_type, id=id, **kwargs)
@@ -1,5 +1,5 @@
1
- import logging
2
1
  import json
2
+ import logging
3
3
 
4
4
  from aiohttp import BasicAuth, ClientSession
5
5
  from sqlalchemy import (
@@ -7,14 +7,17 @@ from sqlalchemy import (
7
7
  Column,
8
8
  DateTime,
9
9
  Enum,
10
+ MetaData,
11
+ Table,
10
12
  Text,
11
- text,
12
13
  TypeDecorator,
13
- Table,
14
- MetaData,
14
+ text,
15
15
  )
16
+ from sqlalchemy.dialects.postgresql import ARRAY, JSONB
17
+ from sqlalchemy.dialects.postgresql import dialect as postgresql_dialect
16
18
  from sqlalchemy.sql.elements import ClauseElement
17
- from sqlalchemy.dialects.postgresql import JSONB, ARRAY, dialect as postgresql_dialect
19
+
20
+ from aidbox_python_sdk.settings import Settings
18
21
 
19
22
  from .exceptions import AidboxDBException
20
23
 
@@ -34,11 +37,9 @@ class _JSONB(TypeDecorator):
34
37
  def process_literal_param(self, value, dialect):
35
38
  if isinstance(value, dict):
36
39
  return "'{}'".format(json.dumps(value).replace("'", "''"))
37
- elif isinstance(value, str):
40
+ if isinstance(value, str):
38
41
  return value
39
- raise ValueError(
40
- "Don't know how to literal-quote " "value of type {}".format(type(value))
41
- )
42
+ raise ValueError(f"Don't know how to literal-quote value of type {type(value)}")
42
43
 
43
44
 
44
45
  class _ARRAY(TypeDecorator):
@@ -46,12 +47,10 @@ class _ARRAY(TypeDecorator):
46
47
 
47
48
  def process_literal_param(self, value, dialect):
48
49
  if isinstance(value, list):
49
- return "ARRAY{}".format(value)
50
- elif isinstance(value, str):
50
+ return f"ARRAY{value}"
51
+ if isinstance(value, str):
51
52
  return value
52
- raise ValueError(
53
- "Don't know how to literal-quote value of type {}".format(type(value))
54
- )
53
+ raise ValueError(f"Don't know how to literal-quote value of type {type(value)}")
55
54
 
56
55
 
57
56
  def create_table(table_name):
@@ -69,17 +68,18 @@ def create_table(table_name):
69
68
  nullable=False,
70
69
  ),
71
70
  Column("resource", _JSONB(astext_type=Text()), nullable=False, index=True),
72
- extend_existing=True
71
+ extend_existing=True,
73
72
  )
74
73
 
75
74
 
76
- class DBProxy(object):
75
+ class DBProxy:
77
76
  _client = None
78
77
  _settings = None
79
- _table_cache = {}
78
+ _table_cache = None
80
79
 
81
- def __init__(self, settings):
80
+ def __init__(self, settings: Settings):
82
81
  self._settings = settings
82
+ self._table_cache = {}
83
83
 
84
84
  async def initialize(self):
85
85
  basic_auth = BasicAuth(
@@ -103,19 +103,17 @@ class DBProxy(object):
103
103
  if not self._client:
104
104
  raise ValueError("Client not set")
105
105
  if not isinstance(sql_query, str):
106
- ValueError("sql_query must be a str")
106
+ raise ValueError("sql_query must be a str")
107
107
  if not execute and sql_query.count(";") > 1:
108
- logger.warning(
109
- "Check that your query does not " "contain two queries separated by `;`"
110
- )
111
- query_url = "{}/$psql".format(self._settings.APP_INIT_URL)
108
+ logger.warning("Check that your query does not contain two queries separated by `;`")
109
+ query_url = f"{self._settings.APP_INIT_URL}/$psql"
112
110
  async with self._client.post(
113
111
  query_url,
114
112
  json={"query": sql_query},
115
113
  params={"execute": "true"} if execute else {},
116
114
  raise_for_status=True,
117
115
  ) as resp:
118
- logger.debug("$psql answer {0}".format(await resp.text()))
116
+ logger.debug("$psql answer %s", await resp.text())
119
117
  results = await resp.json()
120
118
 
121
119
  if results[0]["status"] == "error":
@@ -132,16 +130,14 @@ class DBProxy(object):
132
130
 
133
131
  async def alchemy(self, statement, *, execute=False):
134
132
  if not isinstance(statement, ClauseElement):
135
- ValueError("statement must be a sqlalchemy expression")
133
+ raise ValueError("statement must be a sqlalchemy expression")
136
134
  query = self.compile_statement(statement)
137
135
  logger.debug("Built query:\n%s", query)
138
136
  return await self.raw_sql(query, execute=execute)
139
137
 
140
138
  async def _get_all_entities_name(self):
141
139
  # TODO: refactor using sdk.client and fetch_all
142
- query_url = "{}/Entity?type=resource&_elements=id&_count=999".format(
143
- self._settings.APP_INIT_URL
144
- )
140
+ query_url = f"{self._settings.APP_INIT_URL}/Entity?type=resource&_elements=id&_count=999"
145
141
  async with self._client.get(query_url, raise_for_status=True) as resp:
146
142
  json_resp = await resp.json()
147
143
  return [entry["resource"]["id"] for entry in json_resp["entry"]]
@@ -149,14 +145,9 @@ class DBProxy(object):
149
145
  async def _init_table_cache(self):
150
146
  table_names = await self._get_all_entities_name()
151
147
  self._table_cache = {
148
+ **{table_name: {"table_name": table_name.lower()} for table_name in table_names},
152
149
  **{
153
- table_name: {"table_name": table_name.lower()}
154
- for table_name in table_names
155
- },
156
- **{
157
- "{}History".format(table_name): {
158
- "table_name": "{}_history".format(table_name.lower())
159
- }
150
+ f"{table_name}History": {"table_name": f"{table_name.lower()}_history"}
160
151
  for table_name in table_names
161
152
  },
162
153
  }
@@ -1,4 +1,4 @@
1
- class AidboxSDKException(Exception):
1
+ class AidboxSDKException(Exception): # noqa: N818
2
2
  pass
3
3
 
4
4
 
@@ -1,6 +1,6 @@
1
- import logging
2
1
  import asyncio
3
- import os
2
+ import logging
3
+
4
4
  from aiohttp import web
5
5
  from fhirpy.base.exceptions import OperationOutcome
6
6
 
@@ -9,13 +9,13 @@ routes = web.RouteTableDef()
9
9
 
10
10
 
11
11
  async def subscription(request, data):
12
- logger.debug("Subscription handler: {}".format(data["handler"]))
12
+ logger.debug("Subscription handler: %s", data["handler"])
13
13
  if "handler" not in data or "event" not in data:
14
- logger.error("`handler` and/or `event` param is missing, data: {}".format(data))
14
+ logger.error("`handler` and/or `event` param is missing, data: %s", data)
15
15
  raise web.HTTPBadRequest()
16
16
  handler = request.app["sdk"].get_subscription_handler(data["handler"])
17
17
  if not handler:
18
- logger.error("Subscription handler `{}` was not found".format(data["handler"]))
18
+ logger.error("Subscription handler `%s` was not found", "handler")
19
19
  raise web.HTTPNotFound()
20
20
  result = handler(data["event"], request)
21
21
  if asyncio.iscoroutine(result):
@@ -26,9 +26,7 @@ async def subscription(request, data):
26
26
  async def operation(request, data):
27
27
  logger.debug("Operation handler: %s", data["operation"]["id"])
28
28
  if "operation" not in data or "id" not in data["operation"]:
29
- logger.error(
30
- "`operation` or `operation[id]` param is missing, data: %s", data
31
- )
29
+ logger.error("`operation` or `operation[id]` param is missing, data: %s", data)
32
30
  raise web.HTTPBadRequest()
33
31
  handler = request.app["sdk"].get_operation_handler(data["operation"]["id"])
34
32
  if not handler:
@@ -57,10 +55,10 @@ TYPES = {
57
55
 
58
56
  @routes.post("/aidbox")
59
57
  async def dispatch(request):
60
- logger.debug("Dispatch new request {} {}".format(request.method, request.url))
58
+ logger.debug("Dispatch new request %s %s", request.method, request.url)
61
59
  json = await request.json()
62
60
  if "type" in json and json["type"] in TYPES:
63
- logger.debug("Dispatch to `{}` handler".format(json["type"]))
61
+ logger.debug("Dispatch to `%s` handler", json["type"])
64
62
  return await TYPES[json["type"]](request, json)
65
63
  req = {
66
64
  "method": request.method,
@@ -1,19 +1,17 @@
1
- import asyncio
2
1
  import errno
3
2
  import json
4
3
  import logging
5
- import os
6
4
  import sys
7
5
  from pathlib import Path
8
6
 
9
7
  from aiohttp import BasicAuth, client_exceptions, web
8
+ from fhirpy.base.exceptions import OperationOutcome
10
9
 
11
10
  from .aidboxpy import AsyncAidboxClient
12
11
  from .db import DBProxy
13
12
  from .handlers import routes
14
13
  from .sdk import SDK
15
14
  from .settings import Settings
16
- from fhirpy.base.exceptions import OperationOutcome
17
15
 
18
16
  logger = logging.getLogger("aidbox_sdk")
19
17
  THIS_DIR = Path(__file__).parent
@@ -26,48 +24,41 @@ def setup_routes(app):
26
24
 
27
25
  async def register_app(sdk: SDK, client: AsyncAidboxClient):
28
26
  app_manifest = sdk.build_manifest()
29
-
27
+
30
28
  try:
31
29
  # We create app directly using execute to avoid conversion
32
- await client.execute(
33
- f"/App/{app_manifest['id']}", method="put", data=app_manifest)
34
-
30
+ await client.execute(f"/App/{app_manifest['id']}", method="put", data=app_manifest)
31
+
35
32
  logger.info("Creating seeds and applying migrations")
36
33
  await sdk.create_seed_resources(client)
37
34
  await sdk.apply_migrations(client)
38
35
  logger.info("Aidbox app successfully registered")
39
36
  except OperationOutcome as error:
40
- logger.error(
41
- "Error during the App registration: %s", json.dumps(error, indent=2)
42
- )
37
+ logger.error("Error during the App registration: %s", json.dumps(error, indent=2))
43
38
  sys.exit(errno.EINTR)
44
39
  except (
45
40
  client_exceptions.ServerDisconnectedError,
46
41
  client_exceptions.ClientConnectionError,
47
42
  ):
48
- logger.error(
49
- "Aidbox address is unreachable {}".format(sdk.settings.APP_INIT_URL)
50
- )
43
+ logger.error("Aidbox address is unreachable %s", sdk.settings.APP_INIT_URL)
51
44
  sys.exit(errno.EINTR)
52
45
 
53
46
 
54
47
  async def init_client(settings: Settings):
55
- AidboxClient = settings.AIDBOX_CLIENT_CLASS
48
+ aidbox_client_cls = settings.AIDBOX_CLIENT_CLASS
56
49
  basic_auth = BasicAuth(
57
50
  login=settings.APP_INIT_CLIENT_ID,
58
51
  password=settings.APP_INIT_CLIENT_SECRET,
59
52
  )
60
53
 
61
- return AidboxClient(
62
- "{}".format(settings.APP_INIT_URL), authorization=basic_auth.encode()
63
- )
54
+ return aidbox_client_cls(f"{settings.APP_INIT_URL}", authorization=basic_auth.encode())
64
55
 
65
56
 
66
57
  async def init(app):
67
58
  app["client"] = await init_client(app["settings"])
68
59
  app["db"] = DBProxy(app["settings"])
69
- await app["db"].initialize()
70
60
  await register_app(app["sdk"], app["client"])
61
+ await app["db"].initialize()
71
62
  yield
72
63
  await app["db"].deinitialize()
73
64
 
@@ -1,27 +1,22 @@
1
- import asyncio
2
- import pytest_asyncio
3
- import pytest
4
1
  import os
2
+
3
+ import pytest
4
+ import pytest_asyncio
5
+ from aiohttp import BasicAuth, ClientSession
5
6
  from yarl import URL
6
- from aiohttp import ClientSession, BasicAuth, hdrs
7
- from aiohttp.test_utils import TestServer, TestClient, BaseTestServer
8
- from aiohttp.web import Application
9
- from aiohttp.client import _RequestContextManager
10
7
 
11
8
  from main import create_app as _create_app
12
9
 
13
10
 
14
11
  async def start_app(aiohttp_client):
15
- app = await aiohttp_client(
16
- _create_app(), server_kwargs={"host": "0.0.0.0", "port": 8081}
17
- )
12
+ app = await aiohttp_client(_create_app(), server_kwargs={"host": "0.0.0.0", "port": 8081})
18
13
  sdk = app.server.app["sdk"]
19
14
  sdk._test_start_txid = -1
20
15
 
21
16
  return app
22
17
 
23
18
 
24
- @pytest.fixture
19
+ @pytest.fixture()
25
20
  def client(event_loop, aiohttp_client):
26
21
  """Instance of app's server and client"""
27
22
  return event_loop.run_until_complete(start_app(aiohttp_client))
@@ -29,7 +24,9 @@ def client(event_loop, aiohttp_client):
29
24
 
30
25
  class AidboxSession(ClientSession):
31
26
  def __init__(self, *args, base_url=None, **kwargs):
32
- self.base_url = URL(base_url or os.environ.get("AIDBOX_BASE_URL"))
27
+ base_url_resolved = base_url or os.environ.get("AIDBOX_BASE_URL")
28
+ assert base_url_resolved, "Either base_url arg or AIDBOX_BASE_URL env var must be set"
29
+ self.base_url = URL(base_url_resolved)
33
30
  super().__init__(*args, **kwargs)
34
31
 
35
32
  def make_url(self, path):
@@ -71,18 +68,18 @@ async def safe_db(aidbox, client):
71
68
  sdk._test_start_txid = -1
72
69
  await aidbox.post(
73
70
  "/$psql",
74
- json={"query": "select drop_before_all({});".format(txid)},
71
+ json={"query": f"select drop_before_all({txid});"},
75
72
  params={"execute": "true"},
76
73
  raise_for_status=True,
77
74
  )
78
75
 
79
76
 
80
- @pytest.fixture
77
+ @pytest.fixture()
81
78
  def sdk(client):
82
79
  return client.server.app["sdk"]
83
80
 
84
81
 
85
- @pytest.fixture
82
+ @pytest.fixture()
86
83
  def aidbox_client(client):
87
84
  return client.server.app["client"]
88
85
 
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import logging
3
- import os
4
3
 
5
4
  import jsonschema
6
5
  from fhirpy.base.exceptions import OperationOutcome
@@ -11,8 +10,8 @@ from .db_migrations import sdk_migrations
11
10
  logger = logging.getLogger("aidbox_sdk")
12
11
 
13
12
 
14
- class SDK(object):
15
- def __init__(
13
+ class SDK:
14
+ def __init__( # noqa: PLR0913
16
15
  self,
17
16
  settings,
18
17
  *,
@@ -40,7 +39,7 @@ class SDK(object):
40
39
  self._entities = entities or {}
41
40
  self._seeds = seeds or {}
42
41
  self._migrations = migrations or []
43
- self._app_endpoint_name = "{}-endpoint".format(settings.APP_ID)
42
+ self._app_endpoint_name = f"{settings.APP_ID}-endpoint"
44
43
  self._sub_triggered = {}
45
44
  self._test_start_txid = None
46
45
 
@@ -67,13 +66,11 @@ class SDK(object):
67
66
  resource_id,
68
67
  resource["id"],
69
68
  )
70
- entry = {
71
- "resource": {**resource, "id": resource_id, "resourceType": entity}
72
- }
69
+ entry = {"resource": {**resource, "id": resource_id, "resourceType": entity}}
73
70
  # Conditional create
74
71
  entry["request"] = {
75
72
  "method": "POST",
76
- "url": "/{0}?_id={1}".format(entity, resource_id),
73
+ "url": f"/{entity}?_id={resource_id}",
77
74
  }
78
75
  entries.append(entry)
79
76
  bundle = client.resource("Bundle", type="transaction", entry=entries)
@@ -99,30 +96,27 @@ class SDK(object):
99
96
  if self._test_start_txid is not None:
100
97
  # Skip outside test
101
98
  if self._test_start_txid == -1:
102
- return
99
+ return None
103
100
 
104
101
  # Skip inside another test
105
102
  if int(event["tx"]["id"]) < self._test_start_txid:
106
- return
103
+ return None
107
104
  coro_or_result = func(event, request)
108
105
  if asyncio.iscoroutine(coro_or_result):
109
106
  result = await coro_or_result
110
107
  else:
111
- logger.warning(
112
- "Synchronous subscription handler is deprecated: %s", path
113
- )
108
+ logger.warning("Synchronous subscription handler is deprecated: %s", path)
114
109
  result = coro_or_result
115
110
 
116
111
  if entity in self._sub_triggered:
117
112
  future, counter = self._sub_triggered[entity]
118
113
  if counter > 1:
119
114
  self._sub_triggered[entity] = (future, counter - 1)
115
+ elif future.done():
116
+ pass
117
+ # logger.warning('Uncaught subscription for %s', entity)
120
118
  else:
121
- if future.done():
122
- pass
123
- # logger.warning('Uncaught subscription for %s', entity)
124
- else:
125
- future.set_result(True)
119
+ future.set_result(True)
126
120
 
127
121
  return result
128
122
 
@@ -149,7 +143,7 @@ class SDK(object):
149
143
  def was_subscription_triggered(self, entity):
150
144
  return self.was_subscription_triggered_n_times(entity, 1)
151
145
 
152
- def operation(
146
+ def operation( # noqa: PLR0913
153
147
  self,
154
148
  methods,
155
149
  path,
@@ -158,10 +152,8 @@ class SDK(object):
158
152
  request_schema=None,
159
153
  timeout=None,
160
154
  ):
161
- if public == True and access_policy is not None:
162
- raise ValueError(
163
- "Operation might be public or have access policy, not both"
164
- )
155
+ if public and access_policy is not None:
156
+ raise ValueError("Operation might be public or have access policy, not both")
165
157
 
166
158
  request_validator = None
167
159
  if request_schema:
@@ -177,7 +169,6 @@ class SDK(object):
177
169
  if isinstance(p, str):
178
170
  _str_path.append(p)
179
171
  elif isinstance(p, dict):
180
-
181
172
  _str_path.append("__{}__".format(p["name"]))
182
173
 
183
174
  def wrapped_func(operation, request):
@@ -44,7 +44,7 @@ class Settings:
44
44
  # if not hasattr(self, name):
45
45
  # raise TypeError('{} is not a valid setting name'.format(name))
46
46
  setattr(self, name, value)
47
- setattr(self, "static_path", None)
47
+ self.static_path = None
48
48
 
49
49
  def substitute_environ(self):
50
50
  """
@@ -71,9 +71,9 @@ class Settings:
71
71
  setattr(self, attr_name, env_var)
72
72
  elif is_required and attr_name not in self._custom_settings:
73
73
  raise RuntimeError(
74
- 'The required environment variable "{0}" is currently not set, '
74
+ f'The required environment variable "{env_var_name}" is currently not set, '
75
75
  "you'll need to run `source activate.settings.sh` "
76
76
  "or you can set that single environment variable with "
77
- '`export {0}="<value>"` or pass variable in `custom_settings` '
78
- "argument".format(env_var_name)
77
+ f'`export {env_var_name}="<value>"` or pass variable in `custom_settings` '
78
+ "argument"
79
79
  )
@@ -0,0 +1,16 @@
1
+ from typing_extensions import TypedDict
2
+
3
+ from aidbox_python_sdk.aidboxpy import AsyncAidboxClient
4
+ from aidbox_python_sdk.db import DBProxy
5
+ from aidbox_python_sdk.sdk import SDK
6
+
7
+
8
+ class SDKOperationRequestApp(TypedDict):
9
+ client: AsyncAidboxClient
10
+ db: DBProxy
11
+ sdk: SDK
12
+
13
+
14
+ SDKOperationRequest = TypedDict(
15
+ "SDKOperationRequest", {"app": SDKOperationRequestApp, "route-params": dict, "resource": dict}
16
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aidbox-python-sdk
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Aidbox SDK for python
5
5
  Author-email: "beda.software" <aidbox-python-sdk@beda.software>
6
6
  License: MIT License
@@ -35,33 +35,43 @@ Classifier: Intended Audience :: Developers
35
35
  Classifier: Operating System :: OS Independent
36
36
  Classifier: Programming Language :: Python
37
37
  Classifier: Programming Language :: Python :: 3
38
- Classifier: Programming Language :: Python :: 3.8
39
38
  Classifier: Programming Language :: Python :: 3.9
40
39
  Classifier: Programming Language :: Python :: 3.10
41
40
  Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
42
  Classifier: Topic :: Internet :: WWW/HTTP
43
43
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
44
  Requires-Python: >=3.8
45
45
  Description-Content-Type: text/markdown
46
+ License-File: LICENSE.md
47
+ Requires-Dist: aiohttp>=3.6.2
48
+ Requires-Dist: SQLAlchemy>=1.3.10
49
+ Requires-Dist: fhirpy>=1.3.0
50
+ Requires-Dist: jsonschema>=4.4.0
46
51
  Provides-Extra: test
52
+ Requires-Dist: pytest~=6.0.0; extra == "test"
53
+ Requires-Dist: pytest-asyncio~=0.20.0; extra == "test"
54
+ Requires-Dist: pytest-aiohttp~=1.0.4; extra == "test"
55
+ Requires-Dist: pytest-cov~=4.0.0; extra == "test"
47
56
  Provides-Extra: dev
48
- License-File: LICENSE.md
57
+ Requires-Dist: black; extra == "dev"
58
+ Requires-Dist: autohooks; extra == "dev"
59
+ Requires-Dist: autohooks-plugin-ruff; extra == "dev"
60
+ Requires-Dist: autohooks-plugin-black; extra == "dev"
49
61
 
50
62
  [![build status](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml/badge.svg)](https://github.com/Aidbox/aidbox-python-sdk/actions/workflows/build.yaml)
51
63
  [![pypi](https://img.shields.io/pypi/v/aidbox-python-sdk.svg)](https://pypi.org/project/aidbox-python-sdk/)
52
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
64
+ [![Supported Python version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/release/python-390/)
53
65
 
54
66
  # aidbox-python-sdk
55
67
 
56
- 1. Create a python 3.8+ environment `pyenv `
68
+ 1. Create a python 3.9+ environment `pyenv `
57
69
  2. Set env variables and activate virtual environment `source activate_settings.sh`
58
70
  2. Install the required packages with `pipenv install --dev`
59
71
  3. Make sure the app's settings are configured correctly (see `activate_settings.sh` and `aidbox_python_sdk/settings.py`). You can also
60
72
  use environment variables to define sensitive settings, eg. DB connection variables (see example `.env-ptl`)
61
73
  4. You can then run example with `python example.py`.
62
74
 
63
- Add `APP_FAST_START_MODE=TRUE` to env_tests for fast start mode.
64
-
65
75
  # Getting started
66
76
  ## Minimal application
67
77
  ```Python
@@ -7,12 +7,12 @@ aidbox_python_sdk/aidboxpy.py
7
7
  aidbox_python_sdk/db.py
8
8
  aidbox_python_sdk/db_migrations.py
9
9
  aidbox_python_sdk/exceptions.py
10
- aidbox_python_sdk/gunicorn.py
11
10
  aidbox_python_sdk/handlers.py
12
11
  aidbox_python_sdk/main.py
13
12
  aidbox_python_sdk/pytest_plugin.py
14
13
  aidbox_python_sdk/sdk.py
15
14
  aidbox_python_sdk/settings.py
15
+ aidbox_python_sdk/types.py
16
16
  aidbox_python_sdk.egg-info/PKG-INFO
17
17
  aidbox_python_sdk.egg-info/SOURCES.txt
18
18
  aidbox_python_sdk.egg-info/dependency_links.txt
@@ -0,0 +1,16 @@
1
+ aiohttp>=3.6.2
2
+ SQLAlchemy>=1.3.10
3
+ fhirpy>=1.3.0
4
+ jsonschema>=4.4.0
5
+
6
+ [dev]
7
+ black
8
+ autohooks
9
+ autohooks-plugin-ruff
10
+ autohooks-plugin-black
11
+
12
+ [test]
13
+ pytest~=6.0.0
14
+ pytest-asyncio~=0.20.0
15
+ pytest-aiohttp~=1.0.4
16
+ pytest-cov~=4.0.0
@@ -7,7 +7,7 @@ log_cli_level = "INFO"
7
7
 
8
8
  [tool.autohooks]
9
9
  mode = "pipenv"
10
- pre-commit = ["autohooks.plugins.black", "autohooks.plugins.isort"]
10
+ pre-commit = ["autohooks.plugins.black", "autohooks.plugins.ruff"]
11
11
 
12
12
  [tool.black]
13
13
  line-length = 100
@@ -23,10 +23,23 @@ exclude = '''
23
23
  )
24
24
  '''
25
25
 
26
- [tool.isort]
27
- profile = "black"
28
- line_length = 100
29
- skip = [".gitignore"]
26
+ [tool.ruff]
27
+ target-version = "py311"
28
+ line-length = 100
29
+ extend-exclude = ["example"]
30
+
31
+ [tool.ruff.lint]
32
+ select = ["I", "E", "F", "N", "B", "C4", "PT", "UP", "I001", "A", "RET", "TID251", "RUF", "SIM", "PYI", "T20", "PIE", "G", "ISC", "PL"]
33
+ # E501 is disabled because line limit is controlled by black
34
+ # RUF005 is disabled because we use asyncio tasks without cancelling
35
+ # RUF015 is disabled because index access is preferred way for us
36
+ # RUF019 is disabled because it's needed for pyright
37
+ # PIE804 is disabled because we often use FHIR like camelCase variables
38
+ # SIM102 is disabled because nested if's are more readable
39
+ # SIM117 is disabled because nested with's are more readable
40
+ ignore = ["E501", "RUF006", "RUF015", "RUF019", "PIE804", "SIM102", "SIM117", "PLR2004"]
41
+ unfixable = ["F401"]
42
+
30
43
 
31
44
  [tool.setuptools]
32
45
  zip-safe = false
@@ -50,11 +63,9 @@ authors = [
50
63
  { name = "beda.software", email = "aidbox-python-sdk@beda.software" },
51
64
  ]
52
65
  dependencies = [
53
- "uvloop>=0.13.0",
54
66
  "aiohttp>=3.6.2",
55
67
  "SQLAlchemy>=1.3.10",
56
68
  "fhirpy>=1.3.0",
57
- "coloredlogs",
58
69
  "jsonschema>=4.4.0",
59
70
  ]
60
71
  classifiers = [
@@ -64,10 +75,10 @@ classifiers = [
64
75
  "Operating System :: OS Independent",
65
76
  "Programming Language :: Python",
66
77
  "Programming Language :: Python :: 3",
67
- "Programming Language :: Python :: 3.8",
68
78
  "Programming Language :: Python :: 3.9",
69
79
  "Programming Language :: Python :: 3.10",
70
80
  "Programming Language :: Python :: 3.11",
81
+ "Programming Language :: Python :: 3.12",
71
82
  "Topic :: Internet :: WWW/HTTP",
72
83
  "Topic :: Software Development :: Libraries :: Python Modules",
73
84
  ]
@@ -75,16 +86,15 @@ requires-python = ">=3.8"
75
86
 
76
87
  [project.optional-dependencies]
77
88
  test = [
78
- "pytest>=6.0.0",
79
- "pytest-asyncio>=0.20.0",
80
- "pytest-aiohttp>=1.0.4",
81
- "pytest-cov>=4.0.0",
89
+ "pytest~=6.0.0",
90
+ "pytest-asyncio~=0.20.0",
91
+ "pytest-aiohttp~=1.0.4",
92
+ "pytest-cov~=4.0.0"
82
93
  ]
83
94
  dev = [
84
95
  "black",
85
- "isort",
86
96
  "autohooks",
87
- "autohooks-plugin-isort",
97
+ "autohooks-plugin-ruff",
88
98
  "autohooks-plugin-black",
89
99
  ]
90
100
 
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
- import pytest
3
2
  import logging
4
- from aiohttp import web, ClientSession, BasicAuth
5
3
  from unittest import mock
6
4
 
5
+ import pytest
6
+
7
7
  import main
8
8
 
9
9
 
10
- @pytest.mark.asyncio
10
+ @pytest.mark.asyncio()
11
11
  async def test_health_check(client):
12
12
  resp = await client.get("/health")
13
13
  assert resp.status == 200
@@ -15,7 +15,7 @@ async def test_health_check(client):
15
15
  assert json == {"status": "OK"}
16
16
 
17
17
 
18
- @pytest.mark.asyncio
18
+ @pytest.mark.asyncio()
19
19
  async def test_live_health_check(client):
20
20
  resp = await client.get("/live")
21
21
  assert resp.status == 200
@@ -23,7 +23,7 @@ async def test_live_health_check(client):
23
23
  assert json == {"status": "OK"}
24
24
 
25
25
 
26
- @pytest.mark.skip
26
+ @pytest.mark.skip()
27
27
  async def test_signup_reg_op(client, aidbox):
28
28
  resp = await aidbox.post("signup/register/21.02.19/testvalue")
29
29
  assert resp.status == 200
@@ -34,7 +34,7 @@ async def test_signup_reg_op(client, aidbox):
34
34
  }
35
35
 
36
36
 
37
- @pytest.mark.skip
37
+ @pytest.mark.skip()
38
38
  async def test_appointment_sub(client, aidbox):
39
39
  with mock.patch.object(main, "_appointment_sub") as appointment_sub:
40
40
  f = asyncio.Future()
@@ -65,7 +65,7 @@ async def test_appointment_sub(client, aidbox):
65
65
  assert expected["resource"].items() <= event["resource"].items()
66
66
 
67
67
 
68
- @pytest.mark.asyncio
68
+ @pytest.mark.asyncio()
69
69
  async def test_database_isolation__1(aidbox_client, safe_db):
70
70
  patients = await aidbox_client.resources("Patient").fetch_all()
71
71
  assert len(patients) == 2
@@ -77,7 +77,7 @@ async def test_database_isolation__1(aidbox_client, safe_db):
77
77
  assert len(patients) == 3
78
78
 
79
79
 
80
- @pytest.mark.asyncio
80
+ @pytest.mark.asyncio()
81
81
  async def test_database_isolation__2(aidbox_client, safe_db):
82
82
  patients = await aidbox_client.resources("Patient").fetch_all()
83
83
  assert len(patients) == 2
@@ -1,20 +0,0 @@
1
- """
2
- This file allows your to serve your application using gunicorn. gunicorn is not installed by default
3
- by the requirements file adev creates, you'll need to install it yourself and add it to requirements.txt.
4
-
5
- To run the aidbox_python_sdk using gunicorn, in the terminal run
6
-
7
- pip install gunicorn
8
- gunicorn aidbox_python_sdk.gunicorn:aidbox_python_sdk --worker-class aiohttp.worker.GunicornWebWorker
9
-
10
- You could use a variant of the above with heroku (in the `Procfile`) or with Docker in the ENTRYPOINT statement.
11
- """
12
- import asyncio
13
- import uvloop
14
-
15
- from .main import create_app
16
-
17
- uvloop.install()
18
- loop = asyncio.get_event_loop()
19
-
20
- app = loop.run_until_complete(create_app())
@@ -1,19 +0,0 @@
1
- uvloop>=0.13.0
2
- aiohttp>=3.6.2
3
- SQLAlchemy>=1.3.10
4
- fhirpy>=1.3.0
5
- coloredlogs
6
- jsonschema>=4.4.0
7
-
8
- [dev]
9
- black
10
- isort
11
- autohooks
12
- autohooks-plugin-isort
13
- autohooks-plugin-black
14
-
15
- [test]
16
- pytest>=6.0.0
17
- pytest-asyncio>=0.20.0
18
- pytest-aiohttp>=1.0.4
19
- pytest-cov>=4.0.0