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