SQLAlchemy 2.0.47__cp313-cp313t-win_amd64.whl

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 (274) hide show
  1. sqlalchemy/__init__.py +283 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +184 -0
  4. sqlalchemy/connectors/asyncio.py +429 -0
  5. sqlalchemy/connectors/pyodbc.py +250 -0
  6. sqlalchemy/cyextension/__init__.py +6 -0
  7. sqlalchemy/cyextension/collections.cp313t-win_amd64.pyd +0 -0
  8. sqlalchemy/cyextension/collections.pyx +409 -0
  9. sqlalchemy/cyextension/immutabledict.cp313t-win_amd64.pyd +0 -0
  10. sqlalchemy/cyextension/immutabledict.pxd +8 -0
  11. sqlalchemy/cyextension/immutabledict.pyx +133 -0
  12. sqlalchemy/cyextension/processors.cp313t-win_amd64.pyd +0 -0
  13. sqlalchemy/cyextension/processors.pyx +68 -0
  14. sqlalchemy/cyextension/resultproxy.cp313t-win_amd64.pyd +0 -0
  15. sqlalchemy/cyextension/resultproxy.pyx +102 -0
  16. sqlalchemy/cyextension/util.cp313t-win_amd64.pyd +0 -0
  17. sqlalchemy/cyextension/util.pyx +90 -0
  18. sqlalchemy/dialects/__init__.py +62 -0
  19. sqlalchemy/dialects/_typing.py +30 -0
  20. sqlalchemy/dialects/mssql/__init__.py +88 -0
  21. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  22. sqlalchemy/dialects/mssql/base.py +4093 -0
  23. sqlalchemy/dialects/mssql/information_schema.py +285 -0
  24. sqlalchemy/dialects/mssql/json.py +129 -0
  25. sqlalchemy/dialects/mssql/provision.py +185 -0
  26. sqlalchemy/dialects/mssql/pymssql.py +126 -0
  27. sqlalchemy/dialects/mssql/pyodbc.py +760 -0
  28. sqlalchemy/dialects/mysql/__init__.py +104 -0
  29. sqlalchemy/dialects/mysql/aiomysql.py +250 -0
  30. sqlalchemy/dialects/mysql/asyncmy.py +231 -0
  31. sqlalchemy/dialects/mysql/base.py +3949 -0
  32. sqlalchemy/dialects/mysql/cymysql.py +106 -0
  33. sqlalchemy/dialects/mysql/dml.py +225 -0
  34. sqlalchemy/dialects/mysql/enumerated.py +282 -0
  35. sqlalchemy/dialects/mysql/expression.py +146 -0
  36. sqlalchemy/dialects/mysql/json.py +91 -0
  37. sqlalchemy/dialects/mysql/mariadb.py +72 -0
  38. sqlalchemy/dialects/mysql/mariadbconnector.py +322 -0
  39. sqlalchemy/dialects/mysql/mysqlconnector.py +302 -0
  40. sqlalchemy/dialects/mysql/mysqldb.py +314 -0
  41. sqlalchemy/dialects/mysql/provision.py +153 -0
  42. sqlalchemy/dialects/mysql/pymysql.py +158 -0
  43. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  44. sqlalchemy/dialects/mysql/reflection.py +727 -0
  45. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  46. sqlalchemy/dialects/mysql/types.py +835 -0
  47. sqlalchemy/dialects/oracle/__init__.py +81 -0
  48. sqlalchemy/dialects/oracle/base.py +3802 -0
  49. sqlalchemy/dialects/oracle/cx_oracle.py +1555 -0
  50. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  51. sqlalchemy/dialects/oracle/oracledb.py +941 -0
  52. sqlalchemy/dialects/oracle/provision.py +297 -0
  53. sqlalchemy/dialects/oracle/types.py +316 -0
  54. sqlalchemy/dialects/oracle/vector.py +365 -0
  55. sqlalchemy/dialects/postgresql/__init__.py +167 -0
  56. sqlalchemy/dialects/postgresql/_psycopg_common.py +189 -0
  57. sqlalchemy/dialects/postgresql/array.py +519 -0
  58. sqlalchemy/dialects/postgresql/asyncpg.py +1284 -0
  59. sqlalchemy/dialects/postgresql/base.py +5378 -0
  60. sqlalchemy/dialects/postgresql/dml.py +339 -0
  61. sqlalchemy/dialects/postgresql/ext.py +540 -0
  62. sqlalchemy/dialects/postgresql/hstore.py +406 -0
  63. sqlalchemy/dialects/postgresql/json.py +404 -0
  64. sqlalchemy/dialects/postgresql/named_types.py +524 -0
  65. sqlalchemy/dialects/postgresql/operators.py +129 -0
  66. sqlalchemy/dialects/postgresql/pg8000.py +669 -0
  67. sqlalchemy/dialects/postgresql/pg_catalog.py +326 -0
  68. sqlalchemy/dialects/postgresql/provision.py +183 -0
  69. sqlalchemy/dialects/postgresql/psycopg.py +862 -0
  70. sqlalchemy/dialects/postgresql/psycopg2.py +892 -0
  71. sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
  72. sqlalchemy/dialects/postgresql/ranges.py +1031 -0
  73. sqlalchemy/dialects/postgresql/types.py +313 -0
  74. sqlalchemy/dialects/sqlite/__init__.py +57 -0
  75. sqlalchemy/dialects/sqlite/aiosqlite.py +482 -0
  76. sqlalchemy/dialects/sqlite/base.py +3056 -0
  77. sqlalchemy/dialects/sqlite/dml.py +263 -0
  78. sqlalchemy/dialects/sqlite/json.py +92 -0
  79. sqlalchemy/dialects/sqlite/provision.py +229 -0
  80. sqlalchemy/dialects/sqlite/pysqlcipher.py +157 -0
  81. sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
  82. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  83. sqlalchemy/engine/__init__.py +62 -0
  84. sqlalchemy/engine/_py_processors.py +136 -0
  85. sqlalchemy/engine/_py_row.py +128 -0
  86. sqlalchemy/engine/_py_util.py +74 -0
  87. sqlalchemy/engine/base.py +3390 -0
  88. sqlalchemy/engine/characteristics.py +155 -0
  89. sqlalchemy/engine/create.py +893 -0
  90. sqlalchemy/engine/cursor.py +2298 -0
  91. sqlalchemy/engine/default.py +2394 -0
  92. sqlalchemy/engine/events.py +965 -0
  93. sqlalchemy/engine/interfaces.py +3471 -0
  94. sqlalchemy/engine/mock.py +134 -0
  95. sqlalchemy/engine/processors.py +61 -0
  96. sqlalchemy/engine/reflection.py +2102 -0
  97. sqlalchemy/engine/result.py +2399 -0
  98. sqlalchemy/engine/row.py +400 -0
  99. sqlalchemy/engine/strategies.py +16 -0
  100. sqlalchemy/engine/url.py +924 -0
  101. sqlalchemy/engine/util.py +167 -0
  102. sqlalchemy/event/__init__.py +26 -0
  103. sqlalchemy/event/api.py +220 -0
  104. sqlalchemy/event/attr.py +676 -0
  105. sqlalchemy/event/base.py +472 -0
  106. sqlalchemy/event/legacy.py +258 -0
  107. sqlalchemy/event/registry.py +390 -0
  108. sqlalchemy/events.py +17 -0
  109. sqlalchemy/exc.py +832 -0
  110. sqlalchemy/ext/__init__.py +11 -0
  111. sqlalchemy/ext/associationproxy.py +2027 -0
  112. sqlalchemy/ext/asyncio/__init__.py +25 -0
  113. sqlalchemy/ext/asyncio/base.py +281 -0
  114. sqlalchemy/ext/asyncio/engine.py +1471 -0
  115. sqlalchemy/ext/asyncio/exc.py +21 -0
  116. sqlalchemy/ext/asyncio/result.py +965 -0
  117. sqlalchemy/ext/asyncio/scoping.py +1599 -0
  118. sqlalchemy/ext/asyncio/session.py +1947 -0
  119. sqlalchemy/ext/automap.py +1701 -0
  120. sqlalchemy/ext/baked.py +570 -0
  121. sqlalchemy/ext/compiler.py +600 -0
  122. sqlalchemy/ext/declarative/__init__.py +65 -0
  123. sqlalchemy/ext/declarative/extensions.py +564 -0
  124. sqlalchemy/ext/horizontal_shard.py +478 -0
  125. sqlalchemy/ext/hybrid.py +1535 -0
  126. sqlalchemy/ext/indexable.py +364 -0
  127. sqlalchemy/ext/instrumentation.py +450 -0
  128. sqlalchemy/ext/mutable.py +1085 -0
  129. sqlalchemy/ext/mypy/__init__.py +6 -0
  130. sqlalchemy/ext/mypy/apply.py +324 -0
  131. sqlalchemy/ext/mypy/decl_class.py +515 -0
  132. sqlalchemy/ext/mypy/infer.py +590 -0
  133. sqlalchemy/ext/mypy/names.py +335 -0
  134. sqlalchemy/ext/mypy/plugin.py +303 -0
  135. sqlalchemy/ext/mypy/util.py +357 -0
  136. sqlalchemy/ext/orderinglist.py +439 -0
  137. sqlalchemy/ext/serializer.py +185 -0
  138. sqlalchemy/future/__init__.py +16 -0
  139. sqlalchemy/future/engine.py +15 -0
  140. sqlalchemy/inspection.py +174 -0
  141. sqlalchemy/log.py +288 -0
  142. sqlalchemy/orm/__init__.py +171 -0
  143. sqlalchemy/orm/_orm_constructors.py +2661 -0
  144. sqlalchemy/orm/_typing.py +179 -0
  145. sqlalchemy/orm/attributes.py +2845 -0
  146. sqlalchemy/orm/base.py +971 -0
  147. sqlalchemy/orm/bulk_persistence.py +2135 -0
  148. sqlalchemy/orm/clsregistry.py +571 -0
  149. sqlalchemy/orm/collections.py +1627 -0
  150. sqlalchemy/orm/context.py +3334 -0
  151. sqlalchemy/orm/decl_api.py +2004 -0
  152. sqlalchemy/orm/decl_base.py +2192 -0
  153. sqlalchemy/orm/dependency.py +1302 -0
  154. sqlalchemy/orm/descriptor_props.py +1092 -0
  155. sqlalchemy/orm/dynamic.py +300 -0
  156. sqlalchemy/orm/evaluator.py +379 -0
  157. sqlalchemy/orm/events.py +3252 -0
  158. sqlalchemy/orm/exc.py +237 -0
  159. sqlalchemy/orm/identity.py +302 -0
  160. sqlalchemy/orm/instrumentation.py +754 -0
  161. sqlalchemy/orm/interfaces.py +1496 -0
  162. sqlalchemy/orm/loading.py +1686 -0
  163. sqlalchemy/orm/mapped_collection.py +557 -0
  164. sqlalchemy/orm/mapper.py +4444 -0
  165. sqlalchemy/orm/path_registry.py +809 -0
  166. sqlalchemy/orm/persistence.py +1788 -0
  167. sqlalchemy/orm/properties.py +935 -0
  168. sqlalchemy/orm/query.py +3459 -0
  169. sqlalchemy/orm/relationships.py +3508 -0
  170. sqlalchemy/orm/scoping.py +2148 -0
  171. sqlalchemy/orm/session.py +5280 -0
  172. sqlalchemy/orm/state.py +1168 -0
  173. sqlalchemy/orm/state_changes.py +196 -0
  174. sqlalchemy/orm/strategies.py +3470 -0
  175. sqlalchemy/orm/strategy_options.py +2568 -0
  176. sqlalchemy/orm/sync.py +164 -0
  177. sqlalchemy/orm/unitofwork.py +796 -0
  178. sqlalchemy/orm/util.py +2403 -0
  179. sqlalchemy/orm/writeonly.py +674 -0
  180. sqlalchemy/pool/__init__.py +44 -0
  181. sqlalchemy/pool/base.py +1524 -0
  182. sqlalchemy/pool/events.py +375 -0
  183. sqlalchemy/pool/impl.py +588 -0
  184. sqlalchemy/py.typed +0 -0
  185. sqlalchemy/schema.py +69 -0
  186. sqlalchemy/sql/__init__.py +145 -0
  187. sqlalchemy/sql/_dml_constructors.py +132 -0
  188. sqlalchemy/sql/_elements_constructors.py +1872 -0
  189. sqlalchemy/sql/_orm_types.py +20 -0
  190. sqlalchemy/sql/_py_util.py +75 -0
  191. sqlalchemy/sql/_selectable_constructors.py +763 -0
  192. sqlalchemy/sql/_typing.py +482 -0
  193. sqlalchemy/sql/annotation.py +587 -0
  194. sqlalchemy/sql/base.py +2293 -0
  195. sqlalchemy/sql/cache_key.py +1057 -0
  196. sqlalchemy/sql/coercions.py +1404 -0
  197. sqlalchemy/sql/compiler.py +8081 -0
  198. sqlalchemy/sql/crud.py +1752 -0
  199. sqlalchemy/sql/ddl.py +1444 -0
  200. sqlalchemy/sql/default_comparator.py +551 -0
  201. sqlalchemy/sql/dml.py +1850 -0
  202. sqlalchemy/sql/elements.py +5589 -0
  203. sqlalchemy/sql/events.py +458 -0
  204. sqlalchemy/sql/expression.py +159 -0
  205. sqlalchemy/sql/functions.py +2158 -0
  206. sqlalchemy/sql/lambdas.py +1442 -0
  207. sqlalchemy/sql/naming.py +209 -0
  208. sqlalchemy/sql/operators.py +2623 -0
  209. sqlalchemy/sql/roles.py +323 -0
  210. sqlalchemy/sql/schema.py +6222 -0
  211. sqlalchemy/sql/selectable.py +7265 -0
  212. sqlalchemy/sql/sqltypes.py +3930 -0
  213. sqlalchemy/sql/traversals.py +1024 -0
  214. sqlalchemy/sql/type_api.py +2368 -0
  215. sqlalchemy/sql/util.py +1485 -0
  216. sqlalchemy/sql/visitors.py +1164 -0
  217. sqlalchemy/testing/__init__.py +96 -0
  218. sqlalchemy/testing/assertions.py +994 -0
  219. sqlalchemy/testing/assertsql.py +520 -0
  220. sqlalchemy/testing/asyncio.py +135 -0
  221. sqlalchemy/testing/config.py +434 -0
  222. sqlalchemy/testing/engines.py +483 -0
  223. sqlalchemy/testing/entities.py +117 -0
  224. sqlalchemy/testing/exclusions.py +476 -0
  225. sqlalchemy/testing/fixtures/__init__.py +28 -0
  226. sqlalchemy/testing/fixtures/base.py +384 -0
  227. sqlalchemy/testing/fixtures/mypy.py +332 -0
  228. sqlalchemy/testing/fixtures/orm.py +227 -0
  229. sqlalchemy/testing/fixtures/sql.py +482 -0
  230. sqlalchemy/testing/pickleable.py +155 -0
  231. sqlalchemy/testing/plugin/__init__.py +6 -0
  232. sqlalchemy/testing/plugin/bootstrap.py +51 -0
  233. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  234. sqlalchemy/testing/plugin/pytestplugin.py +892 -0
  235. sqlalchemy/testing/profiling.py +329 -0
  236. sqlalchemy/testing/provision.py +603 -0
  237. sqlalchemy/testing/requirements.py +1945 -0
  238. sqlalchemy/testing/schema.py +198 -0
  239. sqlalchemy/testing/suite/__init__.py +19 -0
  240. sqlalchemy/testing/suite/test_cte.py +237 -0
  241. sqlalchemy/testing/suite/test_ddl.py +389 -0
  242. sqlalchemy/testing/suite/test_deprecations.py +153 -0
  243. sqlalchemy/testing/suite/test_dialect.py +776 -0
  244. sqlalchemy/testing/suite/test_insert.py +630 -0
  245. sqlalchemy/testing/suite/test_reflection.py +3557 -0
  246. sqlalchemy/testing/suite/test_results.py +504 -0
  247. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  248. sqlalchemy/testing/suite/test_select.py +2010 -0
  249. sqlalchemy/testing/suite/test_sequence.py +317 -0
  250. sqlalchemy/testing/suite/test_types.py +2147 -0
  251. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  252. sqlalchemy/testing/suite/test_update_delete.py +139 -0
  253. sqlalchemy/testing/util.py +535 -0
  254. sqlalchemy/testing/warnings.py +52 -0
  255. sqlalchemy/types.py +74 -0
  256. sqlalchemy/util/__init__.py +162 -0
  257. sqlalchemy/util/_collections.py +712 -0
  258. sqlalchemy/util/_concurrency_py3k.py +288 -0
  259. sqlalchemy/util/_has_cy.py +40 -0
  260. sqlalchemy/util/_py_collections.py +541 -0
  261. sqlalchemy/util/compat.py +421 -0
  262. sqlalchemy/util/concurrency.py +110 -0
  263. sqlalchemy/util/deprecations.py +401 -0
  264. sqlalchemy/util/langhelpers.py +2203 -0
  265. sqlalchemy/util/preloaded.py +150 -0
  266. sqlalchemy/util/queue.py +322 -0
  267. sqlalchemy/util/tool_support.py +201 -0
  268. sqlalchemy/util/topological.py +120 -0
  269. sqlalchemy/util/typing.py +734 -0
  270. sqlalchemy-2.0.47.dist-info/METADATA +243 -0
  271. sqlalchemy-2.0.47.dist-info/RECORD +274 -0
  272. sqlalchemy-2.0.47.dist-info/WHEEL +5 -0
  273. sqlalchemy-2.0.47.dist-info/licenses/LICENSE +19 -0
  274. sqlalchemy-2.0.47.dist-info/top_level.txt +1 -0
@@ -0,0 +1,364 @@
1
+ # ext/indexable.py
2
+ # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of SQLAlchemy and is released under
6
+ # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
+
8
+ """Define attributes on ORM-mapped classes that have "index" attributes for
9
+ columns with :class:`_types.Indexable` types.
10
+
11
+ "index" means the attribute is associated with an element of an
12
+ :class:`_types.Indexable` column with the predefined index to access it.
13
+ The :class:`_types.Indexable` types include types such as
14
+ :class:`_types.ARRAY`, :class:`_types.JSON` and
15
+ :class:`_postgresql.HSTORE`.
16
+
17
+
18
+
19
+ The :mod:`~sqlalchemy.ext.indexable` extension provides
20
+ :class:`_schema.Column`-like interface for any element of an
21
+ :class:`_types.Indexable` typed column. In simple cases, it can be
22
+ treated as a :class:`_schema.Column` - mapped attribute.
23
+
24
+ Synopsis
25
+ ========
26
+
27
+ Given ``Person`` as a model with a primary key and JSON data field.
28
+ While this field may have any number of elements encoded within it,
29
+ we would like to refer to the element called ``name`` individually
30
+ as a dedicated attribute which behaves like a standalone column::
31
+
32
+ from sqlalchemy import Column, JSON, Integer
33
+ from sqlalchemy.ext.declarative import declarative_base
34
+ from sqlalchemy.ext.indexable import index_property
35
+
36
+ Base = declarative_base()
37
+
38
+
39
+ class Person(Base):
40
+ __tablename__ = "person"
41
+
42
+ id = Column(Integer, primary_key=True)
43
+ data = Column(JSON)
44
+
45
+ name = index_property("data", "name")
46
+
47
+ Above, the ``name`` attribute now behaves like a mapped column. We
48
+ can compose a new ``Person`` and set the value of ``name``::
49
+
50
+ >>> person = Person(name="Alchemist")
51
+
52
+ The value is now accessible::
53
+
54
+ >>> person.name
55
+ 'Alchemist'
56
+
57
+ Behind the scenes, the JSON field was initialized to a new blank dictionary
58
+ and the field was set::
59
+
60
+ >>> person.data
61
+ {'name': 'Alchemist'}
62
+
63
+ The field is mutable in place::
64
+
65
+ >>> person.name = "Renamed"
66
+ >>> person.name
67
+ 'Renamed'
68
+ >>> person.data
69
+ {'name': 'Renamed'}
70
+
71
+ When using :class:`.index_property`, the change that we make to the indexable
72
+ structure is also automatically tracked as history; we no longer need
73
+ to use :class:`~.mutable.MutableDict` in order to track this change
74
+ for the unit of work.
75
+
76
+ Deletions work normally as well::
77
+
78
+ >>> del person.name
79
+ >>> person.data
80
+ {}
81
+
82
+ Above, deletion of ``person.name`` deletes the value from the dictionary,
83
+ but not the dictionary itself.
84
+
85
+ A missing key will produce ``AttributeError``::
86
+
87
+ >>> person = Person()
88
+ >>> person.name
89
+ AttributeError: 'name'
90
+
91
+ Unless you set a default value::
92
+
93
+ >>> class Person(Base):
94
+ ... __tablename__ = "person"
95
+ ...
96
+ ... id = Column(Integer, primary_key=True)
97
+ ... data = Column(JSON)
98
+ ...
99
+ ... name = index_property("data", "name", default=None) # See default
100
+
101
+ >>> person = Person()
102
+ >>> print(person.name)
103
+ None
104
+
105
+
106
+ The attributes are also accessible at the class level.
107
+ Below, we illustrate ``Person.name`` used to generate
108
+ an indexed SQL criteria::
109
+
110
+ >>> from sqlalchemy.orm import Session
111
+ >>> session = Session()
112
+ >>> query = session.query(Person).filter(Person.name == "Alchemist")
113
+
114
+ The above query is equivalent to::
115
+
116
+ >>> query = session.query(Person).filter(Person.data["name"] == "Alchemist")
117
+
118
+ Multiple :class:`.index_property` objects can be chained to produce
119
+ multiple levels of indexing::
120
+
121
+ from sqlalchemy import Column, JSON, Integer
122
+ from sqlalchemy.ext.declarative import declarative_base
123
+ from sqlalchemy.ext.indexable import index_property
124
+
125
+ Base = declarative_base()
126
+
127
+
128
+ class Person(Base):
129
+ __tablename__ = "person"
130
+
131
+ id = Column(Integer, primary_key=True)
132
+ data = Column(JSON)
133
+
134
+ birthday = index_property("data", "birthday")
135
+ year = index_property("birthday", "year")
136
+ month = index_property("birthday", "month")
137
+ day = index_property("birthday", "day")
138
+
139
+ Above, a query such as::
140
+
141
+ q = session.query(Person).filter(Person.year == "1980")
142
+
143
+ On a PostgreSQL backend, the above query will render as:
144
+
145
+ .. sourcecode:: sql
146
+
147
+ SELECT person.id, person.data
148
+ FROM person
149
+ WHERE person.data -> %(data_1)s -> %(param_1)s = %(param_2)s
150
+
151
+ Default Values
152
+ ==============
153
+
154
+ :class:`.index_property` includes special behaviors for when the indexed
155
+ data structure does not exist, and a set operation is called:
156
+
157
+ * For an :class:`.index_property` that is given an integer index value,
158
+ the default data structure will be a Python list of ``None`` values,
159
+ at least as long as the index value; the value is then set at its
160
+ place in the list. This means for an index value of zero, the list
161
+ will be initialized to ``[None]`` before setting the given value,
162
+ and for an index value of five, the list will be initialized to
163
+ ``[None, None, None, None, None]`` before setting the fifth element
164
+ to the given value. Note that an existing list is **not** extended
165
+ in place to receive a value.
166
+
167
+ * for an :class:`.index_property` that is given any other kind of index
168
+ value (e.g. strings usually), a Python dictionary is used as the
169
+ default data structure.
170
+
171
+ * The default data structure can be set to any Python callable using the
172
+ :paramref:`.index_property.datatype` parameter, overriding the previous
173
+ rules.
174
+
175
+
176
+ Subclassing
177
+ ===========
178
+
179
+ :class:`.index_property` can be subclassed, in particular for the common
180
+ use case of providing coercion of values or SQL expressions as they are
181
+ accessed. Below is a common recipe for use with a PostgreSQL JSON type,
182
+ where we want to also include automatic casting plus ``astext()``::
183
+
184
+ class pg_json_property(index_property):
185
+ def __init__(self, attr_name, index, cast_type):
186
+ super(pg_json_property, self).__init__(attr_name, index)
187
+ self.cast_type = cast_type
188
+
189
+ def expr(self, model):
190
+ expr = super(pg_json_property, self).expr(model)
191
+ return expr.astext.cast(self.cast_type)
192
+
193
+ The above subclass can be used with the PostgreSQL-specific
194
+ version of :class:`_postgresql.JSON`::
195
+
196
+ from sqlalchemy import Column, Integer
197
+ from sqlalchemy.ext.declarative import declarative_base
198
+ from sqlalchemy.dialects.postgresql import JSON
199
+
200
+ Base = declarative_base()
201
+
202
+
203
+ class Person(Base):
204
+ __tablename__ = "person"
205
+
206
+ id = Column(Integer, primary_key=True)
207
+ data = Column(JSON)
208
+
209
+ age = pg_json_property("data", "age", Integer)
210
+
211
+ The ``age`` attribute at the instance level works as before; however
212
+ when rendering SQL, PostgreSQL's ``->>`` operator will be used
213
+ for indexed access, instead of the usual index operator of ``->``::
214
+
215
+ >>> query = session.query(Person).filter(Person.age < 20)
216
+
217
+ The above query will render:
218
+
219
+ .. sourcecode:: sql
220
+
221
+ SELECT person.id, person.data
222
+ FROM person
223
+ WHERE CAST(person.data ->> %(data_1)s AS INTEGER) < %(param_1)s
224
+
225
+ """ # noqa
226
+
227
+ from __future__ import annotations
228
+
229
+ from typing import Any
230
+ from typing import Callable
231
+ from typing import cast
232
+ from typing import Optional
233
+ from typing import TYPE_CHECKING
234
+ from typing import TypeVar
235
+ from typing import Union
236
+
237
+ from .. import inspect
238
+ from ..ext.hybrid import hybrid_property
239
+ from ..orm.attributes import flag_modified
240
+
241
+ if TYPE_CHECKING:
242
+ from ..sql import SQLColumnExpression
243
+ from ..sql._typing import _HasClauseElement
244
+
245
+
246
+ __all__ = ["index_property"]
247
+
248
+ _T = TypeVar("_T")
249
+
250
+
251
+ class index_property(hybrid_property[_T]):
252
+ """A property generator. The generated property describes an object
253
+ attribute that corresponds to an :class:`_types.Indexable`
254
+ column.
255
+
256
+ .. seealso::
257
+
258
+ :mod:`sqlalchemy.ext.indexable`
259
+
260
+ """
261
+
262
+ _NO_DEFAULT_ARGUMENT = cast(_T, object())
263
+
264
+ def __init__(
265
+ self,
266
+ attr_name: str,
267
+ index: Union[int, str],
268
+ default: _T = _NO_DEFAULT_ARGUMENT,
269
+ datatype: Optional[Callable[[], Any]] = None,
270
+ mutable: bool = True,
271
+ onebased: bool = True,
272
+ ):
273
+ """Create a new :class:`.index_property`.
274
+
275
+ :param attr_name:
276
+ An attribute name of an `Indexable` typed column, or other
277
+ attribute that returns an indexable structure.
278
+ :param index:
279
+ The index to be used for getting and setting this value. This
280
+ should be the Python-side index value for integers.
281
+ :param default:
282
+ A value which will be returned instead of `AttributeError`
283
+ when there is not a value at given index.
284
+ :param datatype: default datatype to use when the field is empty.
285
+ By default, this is derived from the type of index used; a
286
+ Python list for an integer index, or a Python dictionary for
287
+ any other style of index. For a list, the list will be
288
+ initialized to a list of None values that is at least
289
+ ``index`` elements long.
290
+ :param mutable: if False, writes and deletes to the attribute will
291
+ be disallowed.
292
+ :param onebased: assume the SQL representation of this value is
293
+ one-based; that is, the first index in SQL is 1, not zero.
294
+ """
295
+
296
+ if mutable:
297
+ super().__init__(self.fget, self.fset, self.fdel, self.expr)
298
+ else:
299
+ super().__init__(self.fget, None, None, self.expr)
300
+ self.attr_name = attr_name
301
+ self.index = index
302
+ self.default = default
303
+ is_numeric = isinstance(index, int)
304
+ onebased = is_numeric and onebased
305
+
306
+ if datatype is not None:
307
+ self.datatype = datatype
308
+ else:
309
+ if is_numeric:
310
+ self.datatype = lambda: [None for x in range(index + 1)] # type: ignore[operator] # noqa: E501
311
+ else:
312
+ self.datatype = dict
313
+ self.onebased = onebased
314
+
315
+ def _fget_default(self, err: Optional[BaseException] = None) -> _T:
316
+ if self.default == self._NO_DEFAULT_ARGUMENT:
317
+ raise AttributeError(self.attr_name) from err
318
+ else:
319
+ return self.default
320
+
321
+ def fget(self, __instance: Any) -> _T:
322
+ attr_name = self.attr_name
323
+ column_value = getattr(__instance, attr_name)
324
+ if column_value is None:
325
+ return self._fget_default()
326
+ try:
327
+ value = column_value[self.index]
328
+ except (KeyError, IndexError) as err:
329
+ return self._fget_default(err)
330
+ else:
331
+ return value # type: ignore[no-any-return]
332
+
333
+ def fset(self, instance: Any, value: _T) -> None:
334
+ attr_name = self.attr_name
335
+ column_value = getattr(instance, attr_name, None)
336
+ if column_value is None:
337
+ column_value = self.datatype()
338
+ setattr(instance, attr_name, column_value)
339
+ column_value[self.index] = value
340
+ setattr(instance, attr_name, column_value)
341
+ if attr_name in inspect(instance).mapper.attrs:
342
+ flag_modified(instance, attr_name)
343
+
344
+ def fdel(self, instance: Any) -> None:
345
+ attr_name = self.attr_name
346
+ column_value = getattr(instance, attr_name)
347
+ if column_value is None:
348
+ raise AttributeError(self.attr_name)
349
+ try:
350
+ del column_value[self.index]
351
+ except KeyError as err:
352
+ raise AttributeError(self.attr_name) from err
353
+ else:
354
+ setattr(instance, attr_name, column_value)
355
+ flag_modified(instance, attr_name)
356
+
357
+ def expr(
358
+ self, model: Any
359
+ ) -> Union[_HasClauseElement[_T], SQLColumnExpression[_T]]:
360
+ column = getattr(model, self.attr_name)
361
+ index = self.index
362
+ if self.onebased:
363
+ index += 1 # type: ignore[operator]
364
+ return column[index] # type: ignore[no-any-return]