DAOModel 0.2.0b0__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.
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.1
2
+ Name: DAOModel
3
+ Version: 0.2.0b0
4
+ Summary: Provides an automatic DAO layer for your models
5
+ Keywords: dao,crud,model,database,db,search,query,sql,sqlmodel,sqlalchemy,pydantic
6
+ Author-Email: Cody M Sommer <bassmastacod@gmail.com>
7
+ License: MIT
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Development Status :: 4 - Beta
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Project-URL: Repository, https://github.com/BassMastaCod/DAOModel.git
22
+ Project-URL: Issues, https://github.com/BassMastaCod/DAOModel/issues
23
+ Requires-Python: >=3.7
24
+ Requires-Dist: sqlalchemy
25
+ Requires-Dist: sqlmodel
26
+ Requires-Dist: str_case_util
27
+ Description-Content-Type: text/markdown
28
+
29
+ # DAOModel
30
+ An instant CRUD layer for your Python models (Powered by
31
+ [SQLModel](https://sqlmodel.tiangolo.com/)/
32
+ [Pydantic](https://docs.pydantic.dev/latest/)/
33
+ [SQLAlchemy](https://www.sqlalchemy.org/)).
34
+
35
+ Eliminate repetitive work by auto creating your DAOs.
36
+ There is no need to write SQL queries or recall how to work with
37
+ SQLAlchemy models when you are only looking to do basic functionality.
38
+
39
+ ## Supported Functions
40
+ * `create`
41
+ * `insert`
42
+ * `update`
43
+ * `upsert`
44
+ * if `exists`
45
+ * `get`
46
+ * `find` (supports advanced searching, more details below)
47
+ * `remove`
48
+ * access to `query` (to do anything else not directly supported)
49
+
50
+ ## Features
51
+ * DAOModel expands on SQLModel so no need to learn a new way to define your models.
52
+ * Existing SQLModel, Pydantic, and SQLAlchemy functionality is still accessible for anything not built into DAOModel.
53
+ * Provides many quality of life additions that I found to be repeated throughout my own projects.
54
+
55
+ ## Usage
56
+ ### Develop your SQLModel as usual:
57
+ ```python
58
+ class Customer(SQLModel, table=True):
59
+ id: int = Field(primary_key=True)
60
+ name: str
61
+ ```
62
+ More on this at [SQLModel's Documentation](https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#create-the-table-model-class)
63
+
64
+ ### Inherit DAOModel in place of _SQLModel_
65
+ `DAOModel` acts as a middleman, adding methods to your class.
66
+ Yet `DAOModel` inherits from `SQLModel`
67
+ meaning your object still `isinstance(m, SQLModel)`.
68
+ ```python
69
+ class Customer(DAOModel, table=True):
70
+ id: int = Field(primary_key=True)
71
+ name: str
72
+ ```
73
+
74
+ ### Configure and Initialize your database
75
+ This library doesn't really care how you setup your DB. Skip ahead if you already know how to do so.
76
+ Otherwise, you may find some of the library's built-in functionality useful.
77
+
78
+ #### Create your engine using DAOModel's helper function
79
+ ```python
80
+ engine = create_engine("database.db")
81
+ ```
82
+ This uses SQLite to store your data. If you don't need persistence,
83
+ an in-memory SQLite DB (perfect for testing) is achievable by excluding the path:
84
+ ```python
85
+ engine = create_engine()
86
+ ```
87
+ While good to start, when/if this doesn't meet your needs, please refer to
88
+ [SQLAlchemy's Docs on Engines](https://docs.sqlalchemy.org/core/engines_connections.html)
89
+
90
+ #### Initialize the Engine
91
+ Once you have your engine, you'll need to initialize each of the tables representing your models.
92
+ ```python
93
+ init_db(engine)
94
+ ```
95
+ This is simply a shortcut method of `SQLModel.metadata.create_all(engine)`.
96
+ > **NOTE:** Be sure your Models are all imported (if defined outside of this file)
97
+ > before executing this code or else those tables will not be included.
98
+
99
+ #### Create a DB session
100
+ ```python
101
+ db = Session(engine)
102
+ ```
103
+ Again, this isn't anything that is specific to DAOModel,
104
+ it is common across SQLModel, Flask, etc. so feel free to do this your own way.
105
+ There exist plenty of guides and tutorials or you can start with [SQLAlchemy's Docs on Sessions](https://docs.sqlalchemy.org/orm/session_basics.html)
106
+
107
+ <br>
108
+
109
+ Now you are ready to populate your database, but for that, we are going to use the DAO!
110
+
111
+ ### Create a DAO for your DAOModel
112
+ Creating a DAO is simple enough, but you will need your db session your class that inherits DAOModel.
113
+ ```python
114
+ DAO(Customer, db)
115
+ ```
116
+ > **NOTE:** You pass the Class to DAO, not an instance of the Class
117
+
118
+ So there you have it, You now have a usable DAO layer for your model!
119
+ Let's look at the full code:
120
+
121
+ ```python
122
+ class Customer(DAOModel, table=True):
123
+ id: int = Field(primary_key=True)
124
+ name: str
125
+
126
+ engine = create_engine("database.db")
127
+ init_db(engine)
128
+ db = Session(engine)
129
+ dao = DAO(Customer, db)
130
+ ```
131
+ It may not be exactly what is wanted for your final product, but it gets you up and running quickly.
132
+ Just a few lines is all you need to get started!
133
+
134
+ ### Using the DAO
135
+ The whole premise of an automatic DAO layer is to make your code more straightforward and readable.
136
+ Read the docs for more details, otherwise the following table should give a general understanding.
137
+
138
+ | Action | Method | Under the hood | Example |
139
+ |----------------------------|--------|---------------------------------------|-----------------------------|
140
+ | Create a new object | create | Adds values as new row to DB table | `model = dao.create(23)` |
141
+ | Insert an object | insert | Adds object as new row to DB table | `dao.insert(model)` |
142
+ | Update an object | update | Updates column values of a row | `dao.update(model)` |
143
+ | Update or insert an object | upsert | Updates or inserts row if not present | `dao.upsert(model)` |
144
+ | Check if an object exists | exists | Checks if any rows match object | `if dao.exists(model):` |
145
+ | Get an object | get | Selects row by primary key | `model = dao.get(23)` |
146
+ | Search for objects | find | Selects rows by column values | `results = dao.find("Bob")` |
147
+ | Delete an object | remove | Deletes row from DB table | `dao.remove(model)` |
148
+
149
+ Check out the Sample Code for a more thorough example.
150
+ You can even use it as a template for your own project!
151
+
152
+ ## Searching
153
+ One of the best features about DAOModel is the robust search functionality.
154
+ Without assembling complicated SQL queries, you are able to search by specific columns.
155
+ ```python
156
+ results = dao.find(name="Bob")
157
+ ```
158
+ Said columns can even be foreign keys or columns from related tables
159
+ (provided those columns are defined as searchable).
160
+ Take a look at `test_dao_find.py` to see some examples.
161
+
162
+ ### Is column value set?
163
+ Perhaps you don't want to search for _Bob_, but rather find all customers who do not have any 'name'.
164
+ Using `True` and `False` limits your results to rows having, or not having, a value.
165
+ ```python
166
+ # find all nameless customers
167
+ results = dao.find(name=False)
168
+ ```
169
+
170
+ ### Duplicate (or unique) values
171
+ Sometimes your customers (or other data) gets added more than once.
172
+ Wouldn't it be great to easily find all of these duplicates? Say no more!
173
+ ```python
174
+ # find all customers that share a name with another customer
175
+ results = dao.find(duplicate=Customer.name)
176
+ ```
177
+ Now I can see that I have 3 customers named _Bob_ and 2 named _Joe_ without listing each other customer.
178
+
179
+ Or maybe it's the unique values you wish to see:
180
+ `dao.find(unique=Customer.name)` will provide all customers that don't share a name,
181
+ in this case, excluding all the _Bobs_ and _Joes_.
182
+
183
+ ### Sorting
184
+ The order of your results can easily be specified.
185
+ By default, results are sorted by primary key.
186
+ But you can sort by any column, foreign key, or foreign property you desire.
187
+ ```python
188
+ # sort by name and then id
189
+ results = dao.find(order=(Customer.name, Customer.id))
190
+ ```
191
+ > NOTE: wrap a column with `desc()` to reverse the order
192
+
193
+ ### All of the above
194
+ The previously stated options can be done together if needed:
195
+ ```python
196
+ results = dao.find(name=True, region="US",
197
+ duplicate=Customer.name, unique=Customer.address,
198
+ order=desc(Customer.last_modified))
199
+ ```
200
+
201
+ ### Integrated Pagination
202
+ Pages of results come naturally for searches done with DAOModel DAOs.
203
+ Indicate the number of intended results using the `per_page` argument.
204
+ Optionally specify which page number to retrieve using the `page` argument.
205
+ ```python
206
+ for result in dao.find(page=2, per_page=10):
207
+ ```
208
+ As seen above, the returned SearchResults are directly iterable.
209
+ But they also include properties for the total number of results.
210
+ The page number and number of results per page are also included for convenience.
211
+
212
+ ## Additional Functionality
213
+ ### Commit on Demand
214
+ Each of the modifying actions in the table above will auto-commit.
215
+ However, if you wish to prevent this, include the argument `commit=False`
216
+ In that scenario, you will need to call `dao.commit()` explicitly.
217
+ This is most useful when conducting batch actions, or when you may wish to abort the changes.
218
+
219
+ ### Copying values
220
+ Values (other than the primary key) can be copied from one model instance to another.
221
+ This is done through `model.copy_model(other_model)` or `model.copy_values(**dictionary)`.
222
+ Both the `create` and `get` functionality have copying built-in in the form of `create_with` and `get_with`.
223
+ ```python
224
+ # create a new row but also populate the name column in the same line
225
+ model = dao.create_with(id=52, name="Bob")
226
+
227
+ # select the now existing row but reassign the name
228
+ model = dao.get_with(id=52, name="Joe")
229
+
230
+ # calling get_with does not modify the DB, so you need to explicitly update or commit
231
+ dao.update(model)
232
+ ```
233
+
234
+ ### DAOFactory
235
+ The DAOFactory allows you to easily open and close sessions as needed.
236
+
237
+
238
+ In order to use it, get yourself a [session factory](https://docs.sqlalchemy.org/orm/session_basics.html#using-a-sessionmaker) and then use a `with` statement.
239
+ ```python
240
+ session_factory = sessionmaker(engine)
241
+ with DAOFactory(session_factory) as daos:
242
+ dao = daos[Customer]
243
+ ```
244
+
245
+ Again, this may not fit your needs exactly,
246
+ but you can inherit from DAOFactory in order to create your own solution.
247
+
248
+ ### Auto-increment ID
249
+ The [SQLModel Tutorial](https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/)
250
+ discusses how to have an auto incrementing primary key.
251
+ This library contains a utility called `next_id()` that,
252
+ when passed as an argument, will not specify an ID so that it is auto generated.
253
+ ```python
254
+ model = dao.create(next_id())
255
+ ```
256
+ This code is equivalent to passing `None` as the argument.
257
+ However, the named method makes the line easier to understand.
258
+
259
+ ## Caveats
260
+ Most testing has been completed using SQLite, though since SQLModel/SQLAlchemy
261
+ support other database solutions, DAOModel is expected to as well.
262
+
263
+ Speaking of SQLite, this library configures Foreign Key constraints to be enforced by default in SQLite.
264
+
265
+ Table names are configured to be snake_case which differs from SQLModel.
266
+ This can be adjusted by overridding `def __tablename__` in your own child class.
267
+
268
+ Not all functionality will work as intended through DAOModel.
269
+ If something isn't supported, submit a ticket or pull request.
270
+ And remember that you may always use what you can and then
271
+ override the code or use the query method in DAO to do the rest.
272
+ It should still save you a lot of lines of code.
@@ -0,0 +1,244 @@
1
+ # DAOModel
2
+ An instant CRUD layer for your Python models (Powered by
3
+ [SQLModel](https://sqlmodel.tiangolo.com/)/
4
+ [Pydantic](https://docs.pydantic.dev/latest/)/
5
+ [SQLAlchemy](https://www.sqlalchemy.org/)).
6
+
7
+ Eliminate repetitive work by auto creating your DAOs.
8
+ There is no need to write SQL queries or recall how to work with
9
+ SQLAlchemy models when you are only looking to do basic functionality.
10
+
11
+ ## Supported Functions
12
+ * `create`
13
+ * `insert`
14
+ * `update`
15
+ * `upsert`
16
+ * if `exists`
17
+ * `get`
18
+ * `find` (supports advanced searching, more details below)
19
+ * `remove`
20
+ * access to `query` (to do anything else not directly supported)
21
+
22
+ ## Features
23
+ * DAOModel expands on SQLModel so no need to learn a new way to define your models.
24
+ * Existing SQLModel, Pydantic, and SQLAlchemy functionality is still accessible for anything not built into DAOModel.
25
+ * Provides many quality of life additions that I found to be repeated throughout my own projects.
26
+
27
+ ## Usage
28
+ ### Develop your SQLModel as usual:
29
+ ```python
30
+ class Customer(SQLModel, table=True):
31
+ id: int = Field(primary_key=True)
32
+ name: str
33
+ ```
34
+ More on this at [SQLModel's Documentation](https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#create-the-table-model-class)
35
+
36
+ ### Inherit DAOModel in place of _SQLModel_
37
+ `DAOModel` acts as a middleman, adding methods to your class.
38
+ Yet `DAOModel` inherits from `SQLModel`
39
+ meaning your object still `isinstance(m, SQLModel)`.
40
+ ```python
41
+ class Customer(DAOModel, table=True):
42
+ id: int = Field(primary_key=True)
43
+ name: str
44
+ ```
45
+
46
+ ### Configure and Initialize your database
47
+ This library doesn't really care how you setup your DB. Skip ahead if you already know how to do so.
48
+ Otherwise, you may find some of the library's built-in functionality useful.
49
+
50
+ #### Create your engine using DAOModel's helper function
51
+ ```python
52
+ engine = create_engine("database.db")
53
+ ```
54
+ This uses SQLite to store your data. If you don't need persistence,
55
+ an in-memory SQLite DB (perfect for testing) is achievable by excluding the path:
56
+ ```python
57
+ engine = create_engine()
58
+ ```
59
+ While good to start, when/if this doesn't meet your needs, please refer to
60
+ [SQLAlchemy's Docs on Engines](https://docs.sqlalchemy.org/core/engines_connections.html)
61
+
62
+ #### Initialize the Engine
63
+ Once you have your engine, you'll need to initialize each of the tables representing your models.
64
+ ```python
65
+ init_db(engine)
66
+ ```
67
+ This is simply a shortcut method of `SQLModel.metadata.create_all(engine)`.
68
+ > **NOTE:** Be sure your Models are all imported (if defined outside of this file)
69
+ > before executing this code or else those tables will not be included.
70
+
71
+ #### Create a DB session
72
+ ```python
73
+ db = Session(engine)
74
+ ```
75
+ Again, this isn't anything that is specific to DAOModel,
76
+ it is common across SQLModel, Flask, etc. so feel free to do this your own way.
77
+ There exist plenty of guides and tutorials or you can start with [SQLAlchemy's Docs on Sessions](https://docs.sqlalchemy.org/orm/session_basics.html)
78
+
79
+ <br>
80
+
81
+ Now you are ready to populate your database, but for that, we are going to use the DAO!
82
+
83
+ ### Create a DAO for your DAOModel
84
+ Creating a DAO is simple enough, but you will need your db session your class that inherits DAOModel.
85
+ ```python
86
+ DAO(Customer, db)
87
+ ```
88
+ > **NOTE:** You pass the Class to DAO, not an instance of the Class
89
+
90
+ So there you have it, You now have a usable DAO layer for your model!
91
+ Let's look at the full code:
92
+
93
+ ```python
94
+ class Customer(DAOModel, table=True):
95
+ id: int = Field(primary_key=True)
96
+ name: str
97
+
98
+ engine = create_engine("database.db")
99
+ init_db(engine)
100
+ db = Session(engine)
101
+ dao = DAO(Customer, db)
102
+ ```
103
+ It may not be exactly what is wanted for your final product, but it gets you up and running quickly.
104
+ Just a few lines is all you need to get started!
105
+
106
+ ### Using the DAO
107
+ The whole premise of an automatic DAO layer is to make your code more straightforward and readable.
108
+ Read the docs for more details, otherwise the following table should give a general understanding.
109
+
110
+ | Action | Method | Under the hood | Example |
111
+ |----------------------------|--------|---------------------------------------|-----------------------------|
112
+ | Create a new object | create | Adds values as new row to DB table | `model = dao.create(23)` |
113
+ | Insert an object | insert | Adds object as new row to DB table | `dao.insert(model)` |
114
+ | Update an object | update | Updates column values of a row | `dao.update(model)` |
115
+ | Update or insert an object | upsert | Updates or inserts row if not present | `dao.upsert(model)` |
116
+ | Check if an object exists | exists | Checks if any rows match object | `if dao.exists(model):` |
117
+ | Get an object | get | Selects row by primary key | `model = dao.get(23)` |
118
+ | Search for objects | find | Selects rows by column values | `results = dao.find("Bob")` |
119
+ | Delete an object | remove | Deletes row from DB table | `dao.remove(model)` |
120
+
121
+ Check out the Sample Code for a more thorough example.
122
+ You can even use it as a template for your own project!
123
+
124
+ ## Searching
125
+ One of the best features about DAOModel is the robust search functionality.
126
+ Without assembling complicated SQL queries, you are able to search by specific columns.
127
+ ```python
128
+ results = dao.find(name="Bob")
129
+ ```
130
+ Said columns can even be foreign keys or columns from related tables
131
+ (provided those columns are defined as searchable).
132
+ Take a look at `test_dao_find.py` to see some examples.
133
+
134
+ ### Is column value set?
135
+ Perhaps you don't want to search for _Bob_, but rather find all customers who do not have any 'name'.
136
+ Using `True` and `False` limits your results to rows having, or not having, a value.
137
+ ```python
138
+ # find all nameless customers
139
+ results = dao.find(name=False)
140
+ ```
141
+
142
+ ### Duplicate (or unique) values
143
+ Sometimes your customers (or other data) gets added more than once.
144
+ Wouldn't it be great to easily find all of these duplicates? Say no more!
145
+ ```python
146
+ # find all customers that share a name with another customer
147
+ results = dao.find(duplicate=Customer.name)
148
+ ```
149
+ Now I can see that I have 3 customers named _Bob_ and 2 named _Joe_ without listing each other customer.
150
+
151
+ Or maybe it's the unique values you wish to see:
152
+ `dao.find(unique=Customer.name)` will provide all customers that don't share a name,
153
+ in this case, excluding all the _Bobs_ and _Joes_.
154
+
155
+ ### Sorting
156
+ The order of your results can easily be specified.
157
+ By default, results are sorted by primary key.
158
+ But you can sort by any column, foreign key, or foreign property you desire.
159
+ ```python
160
+ # sort by name and then id
161
+ results = dao.find(order=(Customer.name, Customer.id))
162
+ ```
163
+ > NOTE: wrap a column with `desc()` to reverse the order
164
+
165
+ ### All of the above
166
+ The previously stated options can be done together if needed:
167
+ ```python
168
+ results = dao.find(name=True, region="US",
169
+ duplicate=Customer.name, unique=Customer.address,
170
+ order=desc(Customer.last_modified))
171
+ ```
172
+
173
+ ### Integrated Pagination
174
+ Pages of results come naturally for searches done with DAOModel DAOs.
175
+ Indicate the number of intended results using the `per_page` argument.
176
+ Optionally specify which page number to retrieve using the `page` argument.
177
+ ```python
178
+ for result in dao.find(page=2, per_page=10):
179
+ ```
180
+ As seen above, the returned SearchResults are directly iterable.
181
+ But they also include properties for the total number of results.
182
+ The page number and number of results per page are also included for convenience.
183
+
184
+ ## Additional Functionality
185
+ ### Commit on Demand
186
+ Each of the modifying actions in the table above will auto-commit.
187
+ However, if you wish to prevent this, include the argument `commit=False`
188
+ In that scenario, you will need to call `dao.commit()` explicitly.
189
+ This is most useful when conducting batch actions, or when you may wish to abort the changes.
190
+
191
+ ### Copying values
192
+ Values (other than the primary key) can be copied from one model instance to another.
193
+ This is done through `model.copy_model(other_model)` or `model.copy_values(**dictionary)`.
194
+ Both the `create` and `get` functionality have copying built-in in the form of `create_with` and `get_with`.
195
+ ```python
196
+ # create a new row but also populate the name column in the same line
197
+ model = dao.create_with(id=52, name="Bob")
198
+
199
+ # select the now existing row but reassign the name
200
+ model = dao.get_with(id=52, name="Joe")
201
+
202
+ # calling get_with does not modify the DB, so you need to explicitly update or commit
203
+ dao.update(model)
204
+ ```
205
+
206
+ ### DAOFactory
207
+ The DAOFactory allows you to easily open and close sessions as needed.
208
+
209
+
210
+ In order to use it, get yourself a [session factory](https://docs.sqlalchemy.org/orm/session_basics.html#using-a-sessionmaker) and then use a `with` statement.
211
+ ```python
212
+ session_factory = sessionmaker(engine)
213
+ with DAOFactory(session_factory) as daos:
214
+ dao = daos[Customer]
215
+ ```
216
+
217
+ Again, this may not fit your needs exactly,
218
+ but you can inherit from DAOFactory in order to create your own solution.
219
+
220
+ ### Auto-increment ID
221
+ The [SQLModel Tutorial](https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/)
222
+ discusses how to have an auto incrementing primary key.
223
+ This library contains a utility called `next_id()` that,
224
+ when passed as an argument, will not specify an ID so that it is auto generated.
225
+ ```python
226
+ model = dao.create(next_id())
227
+ ```
228
+ This code is equivalent to passing `None` as the argument.
229
+ However, the named method makes the line easier to understand.
230
+
231
+ ## Caveats
232
+ Most testing has been completed using SQLite, though since SQLModel/SQLAlchemy
233
+ support other database solutions, DAOModel is expected to as well.
234
+
235
+ Speaking of SQLite, this library configures Foreign Key constraints to be enforced by default in SQLite.
236
+
237
+ Table names are configured to be snake_case which differs from SQLModel.
238
+ This can be adjusted by overridding `def __tablename__` in your own child class.
239
+
240
+ Not all functionality will work as intended through DAOModel.
241
+ If something isn't supported, submit a ticket or pull request.
242
+ And remember that you may always use what you can and then
243
+ override the code or use the query method in DAO to do the rest.
244
+ It should still save you a lot of lines of code.
@@ -0,0 +1,183 @@
1
+ from typing import Any, Self, Iterable, Union
2
+ from sqlmodel import SQLModel
3
+ from sqlalchemy import Column
4
+ from str_case_util import Case
5
+ from sqlalchemy.ext.declarative import declared_attr
6
+
7
+ from daomodel.util import reference_of, names_of
8
+
9
+
10
+ class DAOModel(SQLModel):
11
+ """An SQLModel specifically designed to support a DAO."""
12
+
13
+ @declared_attr
14
+ def __tablename__(self) -> str:
15
+ return self.normalized_name()
16
+
17
+ @classmethod
18
+ def normalized_name(cls) -> str:
19
+ """
20
+ A normalized version of this Model name.
21
+
22
+ :return: The model name in snake_case form
23
+ """
24
+ return Case.SNAKE_CASE.format(cls.__name__)
25
+
26
+ @classmethod
27
+ def doc_name(cls) -> str:
28
+ """
29
+ A reader friendly version of this Model name to be used within documentation.
30
+
31
+ :return: The model name in Title Case
32
+ """
33
+ return Case.TITLE_CASE.format(cls.__name__)
34
+
35
+ @classmethod
36
+ def get_pk(cls) -> list[Column]:
37
+ """
38
+ Returns the Columns that comprise the Primary Key for this Model.
39
+
40
+ :return: A list of primary key columns
41
+ """
42
+ return cls.__table__.primary_key
43
+
44
+ @classmethod
45
+ def get_pk_names(cls) -> list[str]:
46
+ """
47
+ Returns the names of Columns that comprise the Primary Key for this Model.
48
+
49
+ :return: A list of str of the primary key
50
+ """
51
+ return names_of(cls.get_pk())
52
+
53
+ def get_pk_values(self) -> tuple[Any, ...]:
54
+ """
55
+ Returns the values that comprise the Primary Key for this instance of the Model.
56
+
57
+ :return: A tuple of primary key values
58
+ """
59
+ return tuple(list(getattr(self, key) for key in names_of(self.get_pk())))
60
+
61
+ def get_pk_dict(self) -> dict[str, Any]:
62
+ """
63
+ Returns the dictionary Primary Keys for this instance of the Model.
64
+
65
+ :return: A dict of primary key names/values
66
+ """
67
+ return self.model_dump(include=set(self.get_pk_names()))
68
+
69
+ @classmethod
70
+ def get_fks(cls) -> set[Column]:
71
+ """
72
+ Returns the Columns of other objects that are represented by Foreign Keys for this Model.
73
+
74
+ :return: An unordered set of columns
75
+ """
76
+ return {fk.column for fk in cls.__table__.foreign_keys}
77
+
78
+ @classmethod
79
+ def get_fk_properties(cls) -> set[Column]:
80
+ """
81
+ Returns the Columns that represent Foreign Keys for this Model.
82
+
83
+ :return: An unordered set of foreign key columns
84
+ """
85
+ return {fk.parent for fk in cls.__table__.foreign_keys}
86
+
87
+ @classmethod
88
+ def get_properties(cls) -> Iterable[Column]:
89
+ """
90
+ Returns all the Columns for this Model.
91
+
92
+ :return: A list of columns
93
+ """
94
+ return cls.__table__.c
95
+
96
+ @classmethod
97
+ def get_searchable_properties(cls) -> Iterable[Column|tuple[Self, ..., Column]]:
98
+ """
99
+ Returns all the Columns for this Model that may be searched using the DAO find function.
100
+
101
+ :return: A list of searchable columns
102
+ """
103
+ return cls.get_properties()
104
+
105
+ @classmethod
106
+ def find_searchable_column(cls, prop: Union[str, Column], foreign_tables: list[Self]) -> Column:
107
+ """
108
+ Returns the specified searchable Column.
109
+
110
+ :param prop: str type reference of the Column or the Column itself
111
+ :param foreign_tables: A list of foreign tables to populated with tables of properties deemed to be foreign
112
+ :return: The searchable Column
113
+ :raises: Unsearchable if the property is not Searchable for this class
114
+ """
115
+ if type(prop) is not str:
116
+ prop = reference_of(prop)
117
+ for column in cls.get_searchable_properties():
118
+ tables = []
119
+ if type(column) is tuple:
120
+ tables = column[:-1]
121
+ column = column[-1]
122
+ if reference_of(column) in [prop, f"{cls.normalized_name()}.{prop}"]:
123
+ foreign_tables.extend([t.__table__ for t in tables])
124
+ if column.table is not cls.__table__:
125
+ foreign_tables.append(column.table)
126
+ return column
127
+ raise Unsearchable(prop, cls)
128
+
129
+ @classmethod
130
+ def pk_values_to_dict(cls, *pk_values) -> dict[str, Any]:
131
+ """
132
+ Converts the primary key values to a dictionary.
133
+
134
+ :param pk_values: The primary key values, in order
135
+ :return: A new dict containing the primary key values
136
+ """
137
+ return dict(zip(cls.get_pk_names(), *pk_values))
138
+
139
+ def copy_model(self, source: Self) -> None:
140
+ """
141
+ Copies all values, except the primary key, from another instance of this Model.
142
+
143
+ :param source: The model instance from which to copy values
144
+ """
145
+ primary_key = set(source.get_pk_names())
146
+ values = source.model_dump(exclude=primary_key)
147
+ self.copy_values(**values)
148
+
149
+ def copy_values(self, **values) -> None:
150
+ """
151
+ Copies all non-pk property values to this Model.
152
+
153
+ :param values: The dict including values to copy
154
+ """
155
+ pk = self.get_pk_names()
156
+ properties = names_of(self.get_properties())
157
+ for k, v in values.items():
158
+ if k in properties and k not in pk:
159
+ setattr(self, k, v)
160
+
161
+ def __eq__(self, other: Self):
162
+ """Instances are determined to be equal based on only their primary key."""
163
+ return self.get_pk_values() == other.get_pk_values() if type(self) == type(other) else False
164
+
165
+ def __hash__(self):
166
+ return hash(self.get_pk_values())
167
+
168
+ def __str__(self):
169
+ """
170
+ str representation of this is a str of the primary key.
171
+ A single-column PK results in a simple str value of said column i.e. "1234"
172
+ A multi-column PK results in a str of tuple of PK values i.e. ("Cod", "123 Lake Way")
173
+ """
174
+ pk_values = self.get_pk_values()
175
+ if len(pk_values) == 1:
176
+ pk_values = pk_values[0]
177
+ return str(pk_values)
178
+
179
+
180
+ class Unsearchable(Exception):
181
+ """Indicates that the Search Query is not allowed for the specified field."""
182
+ def __init__(self, prop: str, model: type(DAOModel)):
183
+ self.detail = f"Cannot search for {prop} of {model.doc_name()}"