kiba-core 0.5.3.dev90__tar.gz → 0.5.3.dev91__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 (146) hide show
  1. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/PKG-INFO +1 -1
  2. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/store/entity_repository.py +12 -2
  3. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/PKG-INFO +1 -1
  4. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/pyproject.toml +1 -1
  5. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/test_entity_repository.py +43 -0
  6. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.github/pull_request_template.md +0 -0
  7. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.github/workflows/deploy.yml +0 -0
  8. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.github/workflows/publish.yml +0 -0
  9. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.github/workflows/pull-request.yml +0 -0
  10. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.github/workflows/release.yml +0 -0
  11. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/.gitignore +0 -0
  12. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/CHANGELOG.md +0 -0
  13. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/Dockerfile +0 -0
  14. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/README.md +0 -0
  15. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/__init__.py +0 -0
  16. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/__init__.py +0 -0
  17. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/api_request.py +0 -0
  18. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/api_response.py +0 -0
  19. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/authorizer.py +0 -0
  20. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/default_routes.py +0 -0
  21. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/health.py +0 -0
  22. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/json_route.py +0 -0
  23. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/__init__.py +0 -0
  24. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/database_connection_middleware.py +0 -0
  25. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/exception_handling_middleware.py +0 -0
  26. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/logging_middleware.py +0 -0
  27. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/origin_ip_middleware.py +0 -0
  28. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/middleware/server_headers_middleware.py +0 -0
  29. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/openapi.py +0 -0
  30. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/rate_limit.py +0 -0
  31. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/request_body.py +0 -0
  32. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/request_context.py +0 -0
  33. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/route.py +0 -0
  34. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/route_auth.py +0 -0
  35. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/route_metadata.py +0 -0
  36. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/streaming_json_route.py +0 -0
  37. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/api/uploaded_file.py +0 -0
  38. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/aws_requester.py +0 -0
  39. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/caching/__init__.py +0 -0
  40. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/caching/cache.py +0 -0
  41. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/caching/dict_cache.py +0 -0
  42. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/caching/file_cache.py +0 -0
  43. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/cron/__init__.py +0 -0
  44. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/cron/api_request_cron_job_processor.py +0 -0
  45. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/cron/cron_executor.py +0 -0
  46. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/cron/cron_job_processor.py +0 -0
  47. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/cron/model.py +0 -0
  48. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/exceptions.py +0 -0
  49. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/http/__init__.py +0 -0
  50. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/http/basic_authentication.py +0 -0
  51. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/http/jwt.py +0 -0
  52. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/http/rest_method.py +0 -0
  53. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/locks/__init__.py +0 -0
  54. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/locks/lock.py +0 -0
  55. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/locks/model.py +0 -0
  56. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/locks/sql.py +0 -0
  57. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/logging.py +0 -0
  58. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/notifications/__init__.py +0 -0
  59. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/notifications/discord_client.py +0 -0
  60. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/notifications/notification_client.py +0 -0
  61. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/notifications/slack_client.py +0 -0
  62. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/py.typed +0 -0
  63. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/__init__.py +0 -0
  64. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/aqs.py +0 -0
  65. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/cosmos.py +0 -0
  66. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/message_queue.py +0 -0
  67. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/message_queue_processor.py +0 -0
  68. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/model.py +0 -0
  69. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/sql.py +0 -0
  70. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/queues/sqs.py +0 -0
  71. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/requester/__init__.py +0 -0
  72. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/requester/requester.py +0 -0
  73. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/s3_manager.py +0 -0
  74. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/service_client.py +0 -0
  75. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/store/__init__.py +0 -0
  76. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/store/database.py +0 -0
  77. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/store/retriever.py +0 -0
  78. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/store/saver.py +0 -0
  79. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/__init__.py +0 -0
  80. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/async_util.py +0 -0
  81. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/chain_util.py +0 -0
  82. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/date_util.py +0 -0
  83. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/dict_util.py +0 -0
  84. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/file_util.py +0 -0
  85. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/hashing_util.py +0 -0
  86. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/http_util.py +0 -0
  87. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/json_util.py +0 -0
  88. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/list_util.py +0 -0
  89. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/process_util.py +0 -0
  90. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/string_util.py +0 -0
  91. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/typing_util.py +0 -0
  92. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/url_util.py +0 -0
  93. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/util/value_holder.py +0 -0
  94. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/web3/__init__.py +0 -0
  95. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/web3/eth_client.py +0 -0
  96. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/web3/eth_client_registry.py +0 -0
  97. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/core/web3/multicall3.py +0 -0
  98. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/SOURCES.txt +0 -0
  99. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/dependency_links.txt +0 -0
  100. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/requires.txt +0 -0
  101. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/top_level.txt +0 -0
  102. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/makefile +0 -0
  103. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/setup.cfg +0 -0
  104. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/__init__.py +0 -0
  105. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_api_response.py +0 -0
  106. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_authorizer.py +0 -0
  107. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_database_connection_middleware.py +0 -0
  108. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_exception_handling_middleware.py +0 -0
  109. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_json_route.py +0 -0
  110. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_openapi.py +0 -0
  111. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_origin_ip_middleware.py +0 -0
  112. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_rate_limit.py +0 -0
  113. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_request_context.py +0 -0
  114. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_route.py +0 -0
  115. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/api/test_streaming_json_route.py +0 -0
  116. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/caching/test_dict_cache.py +0 -0
  117. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/caching/test_file_cache.py +0 -0
  118. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/cron/__init__.py +0 -0
  119. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/cron/test_cron_executor.py +0 -0
  120. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/locks/test_sql_lock.py +0 -0
  121. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/conftest.py +0 -0
  122. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/test_aqs_queue.py +0 -0
  123. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/test_cosmos_queue.py +0 -0
  124. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/test_message_queue_processor.py +0 -0
  125. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/test_sql_queue.py +0 -0
  126. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/queues/test_sqs_queue.py +0 -0
  127. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/requester/__init__.py +0 -0
  128. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/requester/test_requester.py +0 -0
  129. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/test_database.py +0 -0
  130. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/test_exceptions.py +0 -0
  131. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/test_logging.py +0 -0
  132. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/__init__.py +0 -0
  133. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_async_util.py +0 -0
  134. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_chain_util.py +0 -0
  135. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_date_util.py +0 -0
  136. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_dict_util.py +0 -0
  137. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_file_util.py +0 -0
  138. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_json_util.py +0 -0
  139. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_list_util.py +0 -0
  140. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_process_util.py +0 -0
  141. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_string_util.py +0 -0
  142. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/util/test_url_util.py +0 -0
  143. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/web3/__init__.py +0 -0
  144. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/web3/test_eth_client_registry.py +0 -0
  145. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/tests/web3/test_rest_eth_client.py +0 -0
  146. {kiba_core-0.5.3.dev90 → kiba_core-0.5.3.dev91}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev90
3
+ Version: 0.5.3.dev91
4
4
  Summary: Kiba Labs' python utilities
5
5
  Project-URL: repository, https://github.com/kibalabs/core-py
6
6
  Requires-Python: <4,>=3.12
@@ -9,6 +9,7 @@ from sqlalchemy.dialects import sqlite as sqlalchemy_sqlite
9
9
  from sqlalchemy.engine import Result
10
10
  from sqlalchemy.engine import RowMapping
11
11
  from sqlalchemy.sql import Select
12
+ from sqlalchemy.sql.base import ReadOnlyColumnCollection
12
13
 
13
14
  from core.exceptions import KibaException
14
15
  from core.exceptions import NotFoundException
@@ -25,6 +26,7 @@ from core.store.retriever import uuid_from_value
25
26
  from core.util import date_util
26
27
 
27
28
  EntityType = typing.TypeVar('EntityType', bound=BaseModel)
29
+ UpsertUpdateWhere = typing.Callable[[ReadOnlyColumnCollection[str, typing.Any]], sqlalchemy.ColumnElement[bool]] # type: ignore[explicit-any]
28
30
 
29
31
 
30
32
  class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
@@ -135,7 +137,8 @@ class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
135
137
  result = await database.execute(query=self.table.update().where(self.idColumn == idValue).values(updateValues).returning(*self.list_select_columns()), connection=connection)
136
138
  return self.force_from_result(result=result)
137
139
 
138
- async def upsert(self, database: Database, constraintColumnNames: list[str], connection: DatabaseConnection | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
140
+ # NOTE(krishan711): updateWhere gets the conflicting row's new values (`excluded`) and only updates when it is true; when it is false nothing is written and the stored row is returned
141
+ async def upsert(self, database: Database, constraintColumnNames: list[str], connection: DatabaseConnection | None = None, updateWhere: UpsertUpdateWhere | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
139
142
  updateValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=False)
140
143
  insertValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=True)
141
144
  self._add_generated_id(values=insertValues)
@@ -144,9 +147,16 @@ class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
144
147
  doUpdateStatement = insertStatement.on_conflict_do_update(
145
148
  index_elements=constraintColumns,
146
149
  set_=updateValues,
150
+ where=updateWhere(insertStatement.excluded) if updateWhere is not None else None,
147
151
  )
148
152
  result = await database.execute(query=doUpdateStatement.returning(*self.list_select_columns()), connection=connection)
149
- return self.force_from_result(result=result)
153
+ row = result.mappings().first()
154
+ if row is None and updateWhere is not None:
155
+ storedQuery = self.build_select().where(*[column == insertValues[column] for column in constraintColumns])
156
+ row = (await database.execute(query=storedQuery, connection=connection)).mappings().first()
157
+ if row is None:
158
+ raise NotFoundException
159
+ return self.from_row(row=row)
150
160
 
151
161
  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]
152
162
  if not rowDicts:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev90
3
+ Version: 0.5.3.dev91
4
4
  Summary: Kiba Labs' python utilities
5
5
  Project-URL: repository, https://github.com/kibalabs/core-py
6
6
  Requires-Python: <4,>=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiba-core"
3
- version = "0.5.3.dev90"
3
+ version = "0.5.3.dev91"
4
4
  description = "Kiba Labs' python utilities"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12, <4"
@@ -172,6 +172,49 @@ async def test_upsert_inserts_then_updates(database: Database):
172
172
  assert upsertedItem.count == 2
173
173
 
174
174
 
175
+ async def test_upsert_with_false_update_where_keeps_and_returns_the_stored_row(database: Database):
176
+ item = await _create_item(database=database, name='a', count=1)
177
+ async with database.create_transaction() as connection:
178
+ upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda excluded: excluded.count > ItemsTable.c.count, name='a', count=0, amount=5)
179
+ storedItem = await ItemsRepository.get(database=database, connection=connection, idValue=item.itemId)
180
+ assert upsertedItem == item
181
+ assert storedItem == item
182
+
183
+
184
+ async def test_upsert_with_false_update_where_finds_the_stored_row_by_converted_constraint_values(database: Database):
185
+ repository = UpperNameEntityRepository(table=ItemsTable, modelClass=Item)
186
+ async with database.create_transaction() as connection:
187
+ item = await repository.create(database=database, connection=connection, name='a', count=1, amount=1)
188
+ upsertedItem = await repository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda _excluded: sqlalchemy.false(), name='a', count=2, amount=1)
189
+ assert upsertedItem == item
190
+
191
+
192
+ async def test_upsert_with_true_update_where_updates_the_row(database: Database):
193
+ item = await _create_item(database=database, name='a', count=1)
194
+ async with database.create_transaction() as connection:
195
+ upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda excluded: excluded.count > ItemsTable.c.count, name='a', count=2, amount=5)
196
+ assert (upsertedItem.itemId, upsertedItem.createdDate, upsertedItem.count, upsertedItem.amount) == (item.itemId, item.createdDate, 2, 5)
197
+
198
+
199
+ async def test_upsert_with_update_where_still_inserts_new_rows(database: Database):
200
+ async with database.create_transaction() as connection:
201
+ upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda _excluded: sqlalchemy.false(), name='a', count=1, amount=1)
202
+ storedItems = await ItemsRepository.list_many(database=database, connection=connection, fieldFilters=[])
203
+ assert storedItems == [upsertedItem]
204
+
205
+
206
+ async def test_upsert_only_when_values_changed(database: Database):
207
+ def changed(excluded: typing.Any) -> sqlalchemy.ColumnElement[bool]: # type: ignore[explicit-any]
208
+ return sqlalchemy.or_(ItemsTable.c.count.is_distinct_from(excluded.count), ItemsTable.c.expiryDate.is_distinct_from(excluded.expiryDate))
209
+
210
+ async with database.create_transaction() as connection:
211
+ item = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=None)
212
+ unchangedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=None)
213
+ changedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=datetime.datetime(2030, 1, 1, tzinfo=datetime.UTC))
214
+ assert unchangedItem == item
215
+ assert (changedItem.itemId, changedItem.expiryDate) == (item.itemId, datetime.datetime(2030, 1, 1, tzinfo=datetime.UTC))
216
+
217
+
175
218
  async def test_upsert_many_inserts_and_updates(database: Database):
176
219
  item = await _create_item(database=database, name='a')
177
220
  async with database.create_transaction() as connection:
File without changes