kiba-core 0.5.3.dev76__tar.gz → 0.5.3.dev77__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.
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/CHANGELOG.md +5 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/store/database.py +5 -2
- kiba_core-0.5.3.dev77/core/store/entity_repository.py +205 -0
- kiba_core-0.5.3.dev77/core/store/retriever.py +298 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/kiba_core.egg-info/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/kiba_core.egg-info/SOURCES.txt +2 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/pyproject.toml +1 -1
- kiba_core-0.5.3.dev77/tests/test_entity_repository.py +266 -0
- kiba_core-0.5.3.dev76/core/store/retriever.py +0 -206
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/.gitignore +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/README.md +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/database_connection_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/web3/eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/kiba_core.egg-info/requires.txt +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/makefile +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_json_route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/test_message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/test_database.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/test_logging.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/web3/test_eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/tests/web3/test_rest_eth_client.py +0 -0
- {kiba_core-0.5.3.dev76 → kiba_core-0.5.3.dev77}/uv.lock +0 -0
|
@@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- [MINOR] Added `core.store.entity_repository.EntityRepository`, which reads and writes rows of a table as pydantic models: `create`, `update`, `upsert`, `upsert_many` (postgres and sqlite), `delete`, `list_many`, `get_first`, `get`, `get_one` and `get_one_or_none`. It exposes UUID columns as strings and datetimes as aware utc datetimes, dumps pydantic values in JSON columns, generates ids for UUID primary keys and stamps `createdDate`/`updatedDate` when the table has them; subclasses can override `_convert_value_from_db`/`_convert_value_to_db` for other columns
|
|
12
|
+
- [MINOR] Added `UUIDFieldFilter` (accepts `uuid.UUID` or `str` values) and module-level `apply_orders`, `apply_field_filters` and `get_field_filters_conditions` (conditions usable on selects, updates and deletes) to `core.store.retriever`; `Retriever` now uses them
|
|
13
|
+
- [PATCH] Updated `DateFieldFilter` to convert values to utc for the column (naive for columns without a timezone, aware otherwise) so aware datetimes in any timezone compare correctly
|
|
14
|
+
- [PATCH] Updated `IntegerFieldFilter` to bind values with the column's type so values outside the int64 range work with `Numeric` columns on asyncpg
|
|
15
|
+
- [MINOR] Added `Database.get_dialect_name`
|
|
11
16
|
- [PATCH] Fixed `json_route` and `streaming_json_route` returning a 500 when the JSON body isn't an object (e.g. `true` or `[1]`); they now raise `BadRequestException` (400)
|
|
12
17
|
- [MINOR] Added `shouldNotifyRetriedFailures` (default `True`) to `MessageQueueProcessor`: when `False`, failure notifications are only sent once the queue gives up on a message, not for failures it will retry. `MessageQueue.fail_message` now returns whether the queue scheduled another attempt: `SqlMessageQueue` and `CosmosMessageQueue` return `True` until `maxAttempts` is reached, while `SqsMessageQueue` and `AqsMessageQueue` always return `False` as the platform decides on redelivery, so they keep notifying every failure
|
|
13
18
|
- [MAJOR] Updated `CosmosMessageQueue` to retry failed messages like `SqlMessageQueue`: added `maxAttempts` (default 3) and `failureRetryDelaySeconds` (default 60). `fail_message` reschedules the message with linear backoff and stores `lastError`, and a visible message whose lease expired counts as an attempt when it is claimed again. Once `postCount` reaches `maxAttempts` the message is removed, along with its deduplication item. Previously failed or abandoned messages reappeared once their lease expired and were retried forever
|
|
@@ -61,10 +61,13 @@ class Database:
|
|
|
61
61
|
await self._engine.dispose()
|
|
62
62
|
self._engine = None
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def get_dialect_name(self) -> str:
|
|
65
65
|
if not self._engine:
|
|
66
66
|
raise InternalServerErrorException(message='Engine has not been established. Please called collect() first.')
|
|
67
|
-
|
|
67
|
+
return self._engine.dialect.name
|
|
68
|
+
|
|
69
|
+
def now(self, seconds: float = 0) -> sqlalchemy.ColumnElement[datetime.datetime]:
|
|
70
|
+
dialectName = self.get_dialect_name()
|
|
68
71
|
if dialectName == 'postgresql':
|
|
69
72
|
return sqlalchemy.func.now() + datetime.timedelta(seconds=seconds)
|
|
70
73
|
if dialectName == 'sqlite':
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
import uuid
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import sqlalchemy
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from sqlalchemy.dialects import postgresql as sqlalchemy_psql
|
|
8
|
+
from sqlalchemy.dialects import sqlite as sqlalchemy_sqlite
|
|
9
|
+
from sqlalchemy.engine import Result
|
|
10
|
+
from sqlalchemy.engine import RowMapping
|
|
11
|
+
from sqlalchemy.sql import Select
|
|
12
|
+
|
|
13
|
+
from core.exceptions import KibaException
|
|
14
|
+
from core.exceptions import NotFoundException
|
|
15
|
+
from core.store.database import Database
|
|
16
|
+
from core.store.database import DatabaseConnection
|
|
17
|
+
from core.store.retriever import FieldFilter
|
|
18
|
+
from core.store.retriever import Order
|
|
19
|
+
from core.store.retriever import apply_field_filters
|
|
20
|
+
from core.store.retriever import apply_orders
|
|
21
|
+
from core.store.retriever import datetime_to_column_value
|
|
22
|
+
from core.store.retriever import get_field_filters_conditions
|
|
23
|
+
from core.store.retriever import uuid_from_value
|
|
24
|
+
from core.util import date_util
|
|
25
|
+
|
|
26
|
+
EntityType = typing.TypeVar('EntityType', bound=BaseModel)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
|
|
30
|
+
"""Reads and writes rows of a table as pydantic models.
|
|
31
|
+
|
|
32
|
+
UUID columns are exposed as strings, datetimes as aware utc datetimes and pydantic values in JSON columns are dumped
|
|
33
|
+
on write. `createdDate` and `updatedDate` columns are stamped automatically when the table has them. Override
|
|
34
|
+
`_convert_value_from_db` and `_convert_value_to_db` to add conversions for other columns, and `list_select_columns`
|
|
35
|
+
with `_get_field_values` to read extra (e.g. computed) columns into the model.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
table: sqlalchemy.Table,
|
|
41
|
+
modelClass: type[EntityType],
|
|
42
|
+
) -> None:
|
|
43
|
+
self.table = table
|
|
44
|
+
self.modelClass = modelClass
|
|
45
|
+
try:
|
|
46
|
+
self.idColumn = next(column for column in table.columns if column.primary_key)
|
|
47
|
+
except StopIteration:
|
|
48
|
+
raise KibaException(f'Failed to find id column for table: {table.name}')
|
|
49
|
+
|
|
50
|
+
def _convert_value_from_db(self, column: sqlalchemy.Column[typing.Any], value: typing.Any | None) -> typing.Any | None: # type: ignore[explicit-any]
|
|
51
|
+
if value is None:
|
|
52
|
+
return None
|
|
53
|
+
if isinstance(column.type, sqlalchemy.Uuid):
|
|
54
|
+
return str(value)
|
|
55
|
+
if isinstance(column.type, sqlalchemy.DateTime):
|
|
56
|
+
return date_util.datetime_to_utc(dt=value)
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
def _convert_value_to_db(self, column: sqlalchemy.Column[typing.Any], value: typing.Any | None) -> typing.Any | None: # type: ignore[explicit-any]
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(column.type, sqlalchemy.Uuid):
|
|
63
|
+
return uuid_from_value(value=value)
|
|
64
|
+
if isinstance(column.type, sqlalchemy.DateTime):
|
|
65
|
+
return datetime_to_column_value(column=column, dt=value)
|
|
66
|
+
if isinstance(column.type, sqlalchemy.JSON) and isinstance(value, BaseModel):
|
|
67
|
+
return value.model_dump()
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
def list_select_columns(self) -> list[sqlalchemy.ColumnElement[typing.Any]]: # type: ignore[explicit-any]
|
|
71
|
+
return list(self.table.columns)
|
|
72
|
+
|
|
73
|
+
def build_select(self) -> Select[*tuple[typing.Any, ...]]: # type: ignore[explicit-any]
|
|
74
|
+
return sqlalchemy.select(*self.list_select_columns())
|
|
75
|
+
|
|
76
|
+
def _get_field_values(self, row: RowMapping) -> dict[str, typing.Any]: # type: ignore[explicit-any]
|
|
77
|
+
return {column.key: self._convert_value_from_db(column=column, value=row[column]) for column in self.table.columns}
|
|
78
|
+
|
|
79
|
+
def from_row(self, row: RowMapping) -> EntityType:
|
|
80
|
+
return self.modelClass.model_validate(self._get_field_values(row=row))
|
|
81
|
+
|
|
82
|
+
def force_from_result(self, result: Result[typing.Any]) -> EntityType: # type: ignore[explicit-any]
|
|
83
|
+
row = result.mappings().first()
|
|
84
|
+
if row is None:
|
|
85
|
+
raise NotFoundException
|
|
86
|
+
return self.from_row(row=row)
|
|
87
|
+
|
|
88
|
+
def _get_column_names(self) -> str:
|
|
89
|
+
return ', '.join(column.key for column in self.table.columns)
|
|
90
|
+
|
|
91
|
+
def _create_values(self, kwargs: dict[str, typing.Any], shouldAddCreatedDate: bool = False, shouldAddUpdatedDate: bool = False) -> dict[sqlalchemy.Column[typing.Any], typing.Any]: # type: ignore[explicit-any]
|
|
92
|
+
currentDate = date_util.datetime_from_now()
|
|
93
|
+
values: dict[sqlalchemy.Column[typing.Any], typing.Any] = {} # type: ignore[explicit-any]
|
|
94
|
+
# NOTE(krishan711): immutable tables (e.g. event logs) may have no `updatedDate` column, so this only stamps the
|
|
95
|
+
# columns the table actually defines.
|
|
96
|
+
createdDateColumn = self.table.c.get('createdDate')
|
|
97
|
+
if shouldAddCreatedDate and createdDateColumn is not None:
|
|
98
|
+
values[createdDateColumn] = self._convert_value_to_db(column=createdDateColumn, value=currentDate)
|
|
99
|
+
updatedDateColumn = self.table.c.get('updatedDate')
|
|
100
|
+
if shouldAddUpdatedDate and updatedDateColumn is not None:
|
|
101
|
+
values[updatedDateColumn] = self._convert_value_to_db(column=updatedDateColumn, value=currentDate)
|
|
102
|
+
for key, value in kwargs.items():
|
|
103
|
+
column = self.table.c.get(key)
|
|
104
|
+
if column is None:
|
|
105
|
+
raise KibaException(f'Unknown column: {key}. Valid columns are: {self._get_column_names()}')
|
|
106
|
+
values[column] = self._convert_value_to_db(column=column, value=value)
|
|
107
|
+
return values
|
|
108
|
+
|
|
109
|
+
def _add_generated_id(self, values: dict[sqlalchemy.Column[typing.Any], typing.Any]) -> None: # type: ignore[explicit-any]
|
|
110
|
+
if isinstance(self.idColumn.type, sqlalchemy.Uuid) and values.get(self.idColumn) is None:
|
|
111
|
+
values[self.idColumn] = uuid.uuid4()
|
|
112
|
+
|
|
113
|
+
def _create_upsert_insert(self, database: Database) -> sqlalchemy_psql.Insert | sqlalchemy_sqlite.Insert:
|
|
114
|
+
dialectName = database.get_dialect_name()
|
|
115
|
+
if dialectName == 'postgresql':
|
|
116
|
+
return sqlalchemy_psql.insert(self.table)
|
|
117
|
+
if dialectName == 'sqlite':
|
|
118
|
+
return sqlalchemy_sqlite.insert(self.table)
|
|
119
|
+
raise KibaException(f'EntityRepository upserts do not support dialect: {dialectName}')
|
|
120
|
+
|
|
121
|
+
# Writing
|
|
122
|
+
|
|
123
|
+
async def create(self, database: Database, connection: DatabaseConnection | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
|
|
124
|
+
createValues = self._create_values(kwargs=kwargs, shouldAddCreatedDate=True, shouldAddUpdatedDate=True)
|
|
125
|
+
self._add_generated_id(values=createValues)
|
|
126
|
+
result = await database.execute(query=self.table.insert().values(createValues).returning(*self.list_select_columns()), connection=connection)
|
|
127
|
+
return self.force_from_result(result=result)
|
|
128
|
+
|
|
129
|
+
async def update(self, database: Database, connection: DatabaseConnection | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
|
|
130
|
+
updateValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True)
|
|
131
|
+
idValue: typing.Any | None = updateValues.pop(self.idColumn, None) # type: ignore[explicit-any]
|
|
132
|
+
if idValue is None:
|
|
133
|
+
raise KibaException(f'Failed to find id value for update to {self.table.name}')
|
|
134
|
+
result = await database.execute(query=self.table.update().where(self.idColumn == idValue).values(updateValues).returning(*self.list_select_columns()), connection=connection)
|
|
135
|
+
return self.force_from_result(result=result)
|
|
136
|
+
|
|
137
|
+
async def upsert(self, database: Database, constraintColumnNames: list[str], connection: DatabaseConnection | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
|
|
138
|
+
updateValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=False)
|
|
139
|
+
insertValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=True)
|
|
140
|
+
self._add_generated_id(values=insertValues)
|
|
141
|
+
insertStatement = self._create_upsert_insert(database=database).values(insertValues)
|
|
142
|
+
constraintColumns = [self.table.c[columnName] for columnName in constraintColumnNames]
|
|
143
|
+
doUpdateStatement = insertStatement.on_conflict_do_update(
|
|
144
|
+
index_elements=constraintColumns,
|
|
145
|
+
set_=updateValues,
|
|
146
|
+
)
|
|
147
|
+
result = await database.execute(query=doUpdateStatement.returning(*self.list_select_columns()), connection=connection)
|
|
148
|
+
return self.force_from_result(result=result)
|
|
149
|
+
|
|
150
|
+
async def upsert_many(self, database: Database, constraintColumnNames: list[str], rowDicts: list[dict[str, typing.Any]], connection: DatabaseConnection | None = None) -> list[EntityType]: # type: ignore[explicit-any]
|
|
151
|
+
if not rowDicts:
|
|
152
|
+
return []
|
|
153
|
+
insertValuesList = [self._create_values(kwargs=row, shouldAddUpdatedDate=True, shouldAddCreatedDate=True) for row in rowDicts]
|
|
154
|
+
for insertValues in insertValuesList:
|
|
155
|
+
self._add_generated_id(values=insertValues)
|
|
156
|
+
constraintColumns = [self.table.c[columnName] for columnName in constraintColumnNames]
|
|
157
|
+
insertStatement = self._create_upsert_insert(database=database).values(insertValuesList)
|
|
158
|
+
excludedColumnKeys = {self.idColumn.key, 'createdDate'}
|
|
159
|
+
updateColumnKeys = {column.key for insertValues in insertValuesList for column in insertValues} - excludedColumnKeys
|
|
160
|
+
doUpdateStatement = insertStatement.on_conflict_do_update(
|
|
161
|
+
index_elements=constraintColumns,
|
|
162
|
+
set_={key: insertStatement.excluded[key] for key in updateColumnKeys},
|
|
163
|
+
)
|
|
164
|
+
result = await database.execute(query=doUpdateStatement.returning(*self.list_select_columns()), connection=connection)
|
|
165
|
+
return [self.from_row(row=row) for row in result.mappings().all()]
|
|
166
|
+
|
|
167
|
+
async def delete(self, database: Database, fieldFilters: Sequence[FieldFilter], connection: DatabaseConnection | None = None) -> None:
|
|
168
|
+
query = self.table.delete().where(*get_field_filters_conditions(table=self.table, fieldFilters=fieldFilters))
|
|
169
|
+
await database.execute(query=query, connection=connection)
|
|
170
|
+
|
|
171
|
+
# Reading
|
|
172
|
+
|
|
173
|
+
async def list_many(self, database: Database, fieldFilters: Sequence[FieldFilter] | None = None, orders: Sequence[Order] | None = None, limit: int | None = None, offset: int | None = None, connection: DatabaseConnection | None = None) -> list[EntityType]:
|
|
174
|
+
query = self.build_select()
|
|
175
|
+
if fieldFilters is not None:
|
|
176
|
+
query = apply_field_filters(query=query, table=self.table, fieldFilters=fieldFilters)
|
|
177
|
+
if orders is not None:
|
|
178
|
+
query = apply_orders(query=query, table=self.table, orders=orders)
|
|
179
|
+
if limit is not None:
|
|
180
|
+
query = query.limit(limit)
|
|
181
|
+
if offset is not None:
|
|
182
|
+
query = query.offset(offset)
|
|
183
|
+
result = await database.execute(query=query, connection=connection)
|
|
184
|
+
return [self.from_row(row=row) for row in result.mappings()]
|
|
185
|
+
|
|
186
|
+
async def get_first(self, database: Database, fieldFilters: Sequence[FieldFilter] | None = None, orders: Sequence[Order] | None = None, connection: DatabaseConnection | None = None) -> EntityType | None:
|
|
187
|
+
entities = await self.list_many(database=database, fieldFilters=fieldFilters, orders=orders, limit=1, connection=connection)
|
|
188
|
+
return next(iter(entities), None)
|
|
189
|
+
|
|
190
|
+
async def get(self, database: Database, idValue: typing.Any, connection: DatabaseConnection | None = None) -> EntityType: # type: ignore[explicit-any]
|
|
191
|
+
query = self.build_select().where(self.idColumn == self._convert_value_to_db(column=self.idColumn, value=idValue))
|
|
192
|
+
result = await database.execute(query=query, connection=connection)
|
|
193
|
+
return self.force_from_result(result=result)
|
|
194
|
+
|
|
195
|
+
async def get_one(self, database: Database, fieldFilters: Sequence[FieldFilter], connection: DatabaseConnection | None = None) -> EntityType:
|
|
196
|
+
query = apply_field_filters(query=self.build_select(), table=self.table, fieldFilters=fieldFilters)
|
|
197
|
+
result = await database.execute(query=query, connection=connection)
|
|
198
|
+
# TODO(krishan711): raise an exception if there is more than one result
|
|
199
|
+
return self.force_from_result(result=result)
|
|
200
|
+
|
|
201
|
+
async def get_one_or_none(self, database: Database, fieldFilters: Sequence[FieldFilter], connection: DatabaseConnection | None = None) -> EntityType | None:
|
|
202
|
+
try:
|
|
203
|
+
return await self.get_one(database=database, fieldFilters=fieldFilters, connection=connection)
|
|
204
|
+
except NotFoundException:
|
|
205
|
+
return None
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import typing
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
import sqlalchemy
|
|
9
|
+
import sqlalchemy.sql.functions
|
|
10
|
+
from sqlalchemy import Table
|
|
11
|
+
from sqlalchemy.sql import ColumnElement
|
|
12
|
+
from sqlalchemy.sql import Select
|
|
13
|
+
|
|
14
|
+
from core.store.database import Database
|
|
15
|
+
from core.store.database import ResultType
|
|
16
|
+
from core.util import date_util
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Direction(Enum):
|
|
20
|
+
ASCENDING = 'ascending'
|
|
21
|
+
DESCENDING = 'descending'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclasses.dataclass
|
|
25
|
+
class Order:
|
|
26
|
+
fieldName: str
|
|
27
|
+
direction: Direction = Direction.DESCENDING
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass
|
|
31
|
+
class RandomOrder(Order):
|
|
32
|
+
fieldName: str = '__KIBA_RANDOM'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class FieldFilter:
|
|
37
|
+
fieldName: str
|
|
38
|
+
isNull: bool | None = None
|
|
39
|
+
isNotNull: bool | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass
|
|
43
|
+
class StringFieldFilter(FieldFilter):
|
|
44
|
+
eq: str | None = None
|
|
45
|
+
ne: str | None = None
|
|
46
|
+
containedIn: Sequence[str] | None = None
|
|
47
|
+
notContainedIn: Sequence[str] | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclasses.dataclass
|
|
51
|
+
class UUIDFieldFilter(FieldFilter):
|
|
52
|
+
eq: uuid.UUID | str | None = None
|
|
53
|
+
ne: uuid.UUID | str | None = None
|
|
54
|
+
containedIn: Sequence[uuid.UUID | str] | None = None
|
|
55
|
+
notContainedIn: Sequence[uuid.UUID | str] | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclasses.dataclass
|
|
59
|
+
class DateFieldFilter(FieldFilter):
|
|
60
|
+
eq: datetime.datetime | None = None
|
|
61
|
+
ne: datetime.datetime | None = None
|
|
62
|
+
lte: datetime.datetime | None = None
|
|
63
|
+
lt: datetime.datetime | None = None
|
|
64
|
+
gte: datetime.datetime | None = None
|
|
65
|
+
gt: datetime.datetime | None = None
|
|
66
|
+
containedIn: Sequence[datetime.datetime] | None = None
|
|
67
|
+
notContainedIn: Sequence[datetime.datetime] | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclasses.dataclass
|
|
71
|
+
class IntegerFieldFilter(FieldFilter):
|
|
72
|
+
eq: int | None = None
|
|
73
|
+
ne: int | None = None
|
|
74
|
+
lte: int | None = None
|
|
75
|
+
lt: int | None = None
|
|
76
|
+
gte: int | None = None
|
|
77
|
+
gt: int | None = None
|
|
78
|
+
containedIn: Sequence[int] | None = None
|
|
79
|
+
notContainedIn: Sequence[int] | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclasses.dataclass
|
|
83
|
+
class FloatFieldFilter(FieldFilter):
|
|
84
|
+
eq: float | None = None
|
|
85
|
+
ne: float | None = None
|
|
86
|
+
lte: float | None = None
|
|
87
|
+
lt: float | None = None
|
|
88
|
+
gte: float | None = None
|
|
89
|
+
gt: float | None = None
|
|
90
|
+
containedIn: Sequence[float] | None = None
|
|
91
|
+
notContainedIn: Sequence[float] | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclasses.dataclass
|
|
95
|
+
class BooleanFieldFilter(FieldFilter):
|
|
96
|
+
eq: bool | None = None
|
|
97
|
+
ne: bool | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def uuid_from_value(value: uuid.UUID | str) -> uuid.UUID:
|
|
101
|
+
if isinstance(value, uuid.UUID):
|
|
102
|
+
return value
|
|
103
|
+
return uuid.UUID(hex=value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def datetime_to_column_value(column: sqlalchemy.ColumnElement[typing.Any], dt: datetime.datetime) -> datetime.datetime: # type: ignore[explicit-any]
|
|
107
|
+
# NOTE(krishan711): columns without a timezone store naive utc datetimes, columns with one store aware utc datetimes
|
|
108
|
+
if getattr(column.type, 'timezone', False):
|
|
109
|
+
return date_util.datetime_to_utc(dt=dt)
|
|
110
|
+
return date_util.datetime_to_utc_naive_datetime(dt=dt)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def apply_order(query: Select[ResultType], table: Table, order: Order) -> Select[ResultType]:
|
|
114
|
+
if isinstance(order, RandomOrder):
|
|
115
|
+
query = query.order_by(sqlalchemy.sql.functions.random())
|
|
116
|
+
else:
|
|
117
|
+
field = table.c[order.fieldName]
|
|
118
|
+
query = query.order_by(field.asc() if order.direction == Direction.ASCENDING else field.desc())
|
|
119
|
+
return query
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def apply_orders(query: Select[ResultType], table: Table, orders: Sequence[Order]) -> Select[ResultType]:
|
|
123
|
+
for order in orders:
|
|
124
|
+
query = apply_order(query=query, table=table, order=order)
|
|
125
|
+
return query
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_string_field_filter_conditions(table: Table, fieldFilter: StringFieldFilter) -> list[ColumnElement[bool]]:
|
|
129
|
+
field = table.c[fieldFilter.fieldName]
|
|
130
|
+
conditions: list[ColumnElement[bool]] = []
|
|
131
|
+
if fieldFilter.eq is not None:
|
|
132
|
+
conditions.append(field == fieldFilter.eq)
|
|
133
|
+
if fieldFilter.ne is not None:
|
|
134
|
+
conditions.append(field != fieldFilter.ne)
|
|
135
|
+
if fieldFilter.containedIn is not None:
|
|
136
|
+
conditions.append(field.in_(fieldFilter.containedIn))
|
|
137
|
+
if fieldFilter.notContainedIn is not None:
|
|
138
|
+
conditions.append(field.not_in(fieldFilter.notContainedIn))
|
|
139
|
+
return conditions
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_uuid_field_filter_conditions(table: Table, fieldFilter: UUIDFieldFilter) -> list[ColumnElement[bool]]:
|
|
143
|
+
field = table.c[fieldFilter.fieldName]
|
|
144
|
+
conditions: list[ColumnElement[bool]] = []
|
|
145
|
+
if fieldFilter.eq is not None:
|
|
146
|
+
conditions.append(field == uuid_from_value(value=fieldFilter.eq))
|
|
147
|
+
if fieldFilter.ne is not None:
|
|
148
|
+
conditions.append(field != uuid_from_value(value=fieldFilter.ne))
|
|
149
|
+
if fieldFilter.containedIn is not None:
|
|
150
|
+
conditions.append(field.in_([uuid_from_value(value=value) for value in fieldFilter.containedIn]))
|
|
151
|
+
if fieldFilter.notContainedIn is not None:
|
|
152
|
+
conditions.append(field.not_in([uuid_from_value(value=value) for value in fieldFilter.notContainedIn]))
|
|
153
|
+
return conditions
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_date_field_filter_conditions(table: Table, fieldFilter: DateFieldFilter) -> list[ColumnElement[bool]]:
|
|
157
|
+
field = table.c[fieldFilter.fieldName]
|
|
158
|
+
conditions: list[ColumnElement[bool]] = []
|
|
159
|
+
if fieldFilter.eq is not None:
|
|
160
|
+
conditions.append(field == datetime_to_column_value(column=field, dt=fieldFilter.eq))
|
|
161
|
+
if fieldFilter.ne is not None:
|
|
162
|
+
conditions.append(field != datetime_to_column_value(column=field, dt=fieldFilter.ne))
|
|
163
|
+
if fieldFilter.lte is not None:
|
|
164
|
+
conditions.append(field <= datetime_to_column_value(column=field, dt=fieldFilter.lte))
|
|
165
|
+
if fieldFilter.lt is not None:
|
|
166
|
+
conditions.append(field < datetime_to_column_value(column=field, dt=fieldFilter.lt))
|
|
167
|
+
if fieldFilter.gte is not None:
|
|
168
|
+
conditions.append(field >= datetime_to_column_value(column=field, dt=fieldFilter.gte))
|
|
169
|
+
if fieldFilter.gt is not None:
|
|
170
|
+
conditions.append(field > datetime_to_column_value(column=field, dt=fieldFilter.gt))
|
|
171
|
+
if fieldFilter.containedIn is not None:
|
|
172
|
+
conditions.append(field.in_([datetime_to_column_value(column=field, dt=value) for value in fieldFilter.containedIn]))
|
|
173
|
+
if fieldFilter.notContainedIn is not None:
|
|
174
|
+
conditions.append(field.not_in([datetime_to_column_value(column=field, dt=value) for value in fieldFilter.notContainedIn]))
|
|
175
|
+
return conditions
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_integer_field_filter_conditions(table: Table, fieldFilter: IntegerFieldFilter) -> list[ColumnElement[bool]]:
|
|
179
|
+
# NOTE(krishan711): `field == value` / `field.in_(values)` let SQLAlchemy's `Numeric.coerce_compared_value`
|
|
180
|
+
# re-infer a bind param type from the compared Python `int` (typically `BigInteger`, i.e. int64) instead of
|
|
181
|
+
# keeping `field.type` (e.g. `Numeric(78, 0)` for uint256 columns). Binding explicitly with `type_=field.type`
|
|
182
|
+
# avoids an asyncpg `OverflowError` for any value outside the int64 range.
|
|
183
|
+
field = table.c[fieldFilter.fieldName]
|
|
184
|
+
conditions: list[ColumnElement[bool]] = []
|
|
185
|
+
if fieldFilter.eq is not None:
|
|
186
|
+
conditions.append(field == sqlalchemy.bindparam(None, fieldFilter.eq, type_=field.type))
|
|
187
|
+
if fieldFilter.ne is not None:
|
|
188
|
+
conditions.append(field != sqlalchemy.bindparam(None, fieldFilter.ne, type_=field.type))
|
|
189
|
+
if fieldFilter.lte is not None:
|
|
190
|
+
conditions.append(field <= sqlalchemy.bindparam(None, fieldFilter.lte, type_=field.type))
|
|
191
|
+
if fieldFilter.lt is not None:
|
|
192
|
+
conditions.append(field < sqlalchemy.bindparam(None, fieldFilter.lt, type_=field.type))
|
|
193
|
+
if fieldFilter.gte is not None:
|
|
194
|
+
conditions.append(field >= sqlalchemy.bindparam(None, fieldFilter.gte, type_=field.type))
|
|
195
|
+
if fieldFilter.gt is not None:
|
|
196
|
+
conditions.append(field > sqlalchemy.bindparam(None, fieldFilter.gt, type_=field.type))
|
|
197
|
+
if fieldFilter.containedIn is not None:
|
|
198
|
+
conditions.append(field.in_(sqlalchemy.bindparam(None, fieldFilter.containedIn, type_=field.type, expanding=True)))
|
|
199
|
+
if fieldFilter.notContainedIn is not None:
|
|
200
|
+
conditions.append(field.not_in(sqlalchemy.bindparam(None, fieldFilter.notContainedIn, type_=field.type, expanding=True)))
|
|
201
|
+
return conditions
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_float_field_filter_conditions(table: Table, fieldFilter: FloatFieldFilter) -> list[ColumnElement[bool]]:
|
|
205
|
+
field = table.c[fieldFilter.fieldName]
|
|
206
|
+
conditions: list[ColumnElement[bool]] = []
|
|
207
|
+
if fieldFilter.eq is not None:
|
|
208
|
+
conditions.append(field == fieldFilter.eq)
|
|
209
|
+
if fieldFilter.ne is not None:
|
|
210
|
+
conditions.append(field != fieldFilter.ne)
|
|
211
|
+
if fieldFilter.lte is not None:
|
|
212
|
+
conditions.append(field <= fieldFilter.lte)
|
|
213
|
+
if fieldFilter.lt is not None:
|
|
214
|
+
conditions.append(field < fieldFilter.lt)
|
|
215
|
+
if fieldFilter.gte is not None:
|
|
216
|
+
conditions.append(field >= fieldFilter.gte)
|
|
217
|
+
if fieldFilter.gt is not None:
|
|
218
|
+
conditions.append(field > fieldFilter.gt)
|
|
219
|
+
if fieldFilter.containedIn is not None:
|
|
220
|
+
conditions.append(field.in_(fieldFilter.containedIn))
|
|
221
|
+
if fieldFilter.notContainedIn is not None:
|
|
222
|
+
conditions.append(field.not_in(fieldFilter.notContainedIn))
|
|
223
|
+
return conditions
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_boolean_field_filter_conditions(table: Table, fieldFilter: BooleanFieldFilter) -> list[ColumnElement[bool]]:
|
|
227
|
+
field = table.c[fieldFilter.fieldName]
|
|
228
|
+
conditions: list[ColumnElement[bool]] = []
|
|
229
|
+
if fieldFilter.eq is not None:
|
|
230
|
+
conditions.append(field == fieldFilter.eq)
|
|
231
|
+
if fieldFilter.ne is not None:
|
|
232
|
+
conditions.append(field != fieldFilter.ne)
|
|
233
|
+
return conditions
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_field_filter_conditions(table: Table, fieldFilter: FieldFilter) -> list[ColumnElement[bool]]:
|
|
237
|
+
field = table.c[fieldFilter.fieldName]
|
|
238
|
+
conditions: list[ColumnElement[bool]] = []
|
|
239
|
+
if fieldFilter.isNull:
|
|
240
|
+
conditions.append(field.is_(None))
|
|
241
|
+
if fieldFilter.isNotNull:
|
|
242
|
+
conditions.append(field.is_not(None))
|
|
243
|
+
if isinstance(fieldFilter, StringFieldFilter):
|
|
244
|
+
conditions += get_string_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
245
|
+
if isinstance(fieldFilter, UUIDFieldFilter):
|
|
246
|
+
conditions += get_uuid_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
247
|
+
if isinstance(fieldFilter, DateFieldFilter):
|
|
248
|
+
conditions += get_date_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
249
|
+
if isinstance(fieldFilter, IntegerFieldFilter):
|
|
250
|
+
conditions += get_integer_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
251
|
+
if isinstance(fieldFilter, FloatFieldFilter):
|
|
252
|
+
conditions += get_float_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
253
|
+
if isinstance(fieldFilter, BooleanFieldFilter):
|
|
254
|
+
conditions += get_boolean_field_filter_conditions(table=table, fieldFilter=fieldFilter)
|
|
255
|
+
return conditions
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_field_filters_conditions(table: Table, fieldFilters: Sequence[FieldFilter]) -> list[ColumnElement[bool]]:
|
|
259
|
+
return [condition for fieldFilter in fieldFilters for condition in get_field_filter_conditions(table=table, fieldFilter=fieldFilter)]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def apply_field_filters(query: Select[ResultType], table: Table, fieldFilters: Sequence[FieldFilter]) -> Select[ResultType]:
|
|
263
|
+
return query.where(*get_field_filters_conditions(table=table, fieldFilters=fieldFilters))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Retriever:
|
|
267
|
+
def __init__(self, database: Database) -> None:
|
|
268
|
+
self.database = database
|
|
269
|
+
|
|
270
|
+
def _apply_order(self, query: Select[ResultType], table: Table, order: Order) -> Select[ResultType]:
|
|
271
|
+
return apply_order(query=query, table=table, order=order)
|
|
272
|
+
|
|
273
|
+
def _apply_orders(self, query: Select[ResultType], table: Table, orders: Sequence[Order]) -> Select[ResultType]:
|
|
274
|
+
return apply_orders(query=query, table=table, orders=orders)
|
|
275
|
+
|
|
276
|
+
def _apply_string_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: StringFieldFilter) -> Select[ResultType]:
|
|
277
|
+
return query.where(*get_string_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
278
|
+
|
|
279
|
+
def _apply_uuid_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: UUIDFieldFilter) -> Select[ResultType]:
|
|
280
|
+
return query.where(*get_uuid_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
281
|
+
|
|
282
|
+
def _apply_date_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: DateFieldFilter) -> Select[ResultType]:
|
|
283
|
+
return query.where(*get_date_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
284
|
+
|
|
285
|
+
def _apply_integer_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: IntegerFieldFilter) -> Select[ResultType]:
|
|
286
|
+
return query.where(*get_integer_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
287
|
+
|
|
288
|
+
def _apply_float_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: FloatFieldFilter) -> Select[ResultType]:
|
|
289
|
+
return query.where(*get_float_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
290
|
+
|
|
291
|
+
def _apply_boolean_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: BooleanFieldFilter) -> Select[ResultType]:
|
|
292
|
+
return query.where(*get_boolean_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
293
|
+
|
|
294
|
+
def _apply_field_filter(self, query: Select[ResultType], table: Table, fieldFilter: FieldFilter) -> Select[ResultType]:
|
|
295
|
+
return query.where(*get_field_filter_conditions(table=table, fieldFilter=fieldFilter))
|
|
296
|
+
|
|
297
|
+
def _apply_field_filters(self, query: Select[ResultType], table: Table, fieldFilters: Sequence[FieldFilter]) -> Select[ResultType]:
|
|
298
|
+
return apply_field_filters(query=query, table=table, fieldFilters=fieldFilters)
|
|
@@ -69,6 +69,7 @@ core/requester/__init__.py
|
|
|
69
69
|
core/requester/requester.py
|
|
70
70
|
core/store/__init__.py
|
|
71
71
|
core/store/database.py
|
|
72
|
+
core/store/entity_repository.py
|
|
72
73
|
core/store/retriever.py
|
|
73
74
|
core/store/saver.py
|
|
74
75
|
core/util/__init__.py
|
|
@@ -97,6 +98,7 @@ kiba_core.egg-info/requires.txt
|
|
|
97
98
|
kiba_core.egg-info/top_level.txt
|
|
98
99
|
tests/__init__.py
|
|
99
100
|
tests/test_database.py
|
|
101
|
+
tests/test_entity_repository.py
|
|
100
102
|
tests/test_exceptions.py
|
|
101
103
|
tests/test_logging.py
|
|
102
104
|
tests/api/test_api_response.py
|