activemodel 0.3.0__py3-none-any.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.
@@ -0,0 +1,6 @@
1
+ from .base_model import BaseModel
2
+ from .query_wrapper import QueryWrapper
3
+ from .timestamps import TimestampMixin
4
+
5
+ # TODO need a way to specify the session generator
6
+ # TODO need a way to specify the session generator
@@ -0,0 +1,137 @@
1
+ import json
2
+ import typing as t
3
+
4
+ import pydash
5
+ import sqlalchemy as sa
6
+ from sqlalchemy.orm import declared_attr
7
+ from sqlmodel import Session, SQLModel
8
+
9
+ from .query_wrapper import QueryWrapper
10
+
11
+
12
+ class BaseModel(SQLModel):
13
+ """
14
+ Base model class to inherit from so we can hate python less
15
+
16
+ https://github.com/woofz/sqlmodel-basecrud/blob/main/sqlmodel_basecrud/basecrud.py
17
+ """
18
+
19
+ # TODO implement actually calling these hooks
20
+
21
+ def before_delete(self):
22
+ pass
23
+
24
+ def after_delete(self):
25
+ pass
26
+
27
+ def before_save(self):
28
+ pass
29
+
30
+ def after_save(self):
31
+ pass
32
+
33
+ def before_update(self):
34
+ pass
35
+
36
+ def after_update(self):
37
+ pass
38
+
39
+ @declared_attr
40
+ def __tablename__(cls) -> str:
41
+ """
42
+ Automatically generates the table name for the model by converting the class name from camel case to snake case.
43
+
44
+ By default, the class is lower cased which makes it harder to read.
45
+
46
+ Many snake_case libraries struggle with snake case for names like LLMCache, which is why we are using a more
47
+ complicated implementation from pydash.
48
+
49
+ https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
50
+ """
51
+ return pydash.strings.snake_case(cls.__name__)
52
+
53
+ @classmethod
54
+ def select(cls, *args):
55
+ return QueryWrapper[cls](cls, *args)
56
+
57
+ def save(self):
58
+ old_session = Session.object_session(self)
59
+ with get_session() as session:
60
+ if old_session:
61
+ # I was running into an issue where the object was already
62
+ # associated with a session, but the session had been closed,
63
+ # to get around this, you need to remove it from the old one,
64
+ # then add it to the new one (below)
65
+ old_session.expunge(self)
66
+
67
+ self.before_update()
68
+ # self.before_save()
69
+
70
+ session.add(self)
71
+ session.commit()
72
+ session.refresh(self)
73
+
74
+ self.after_update()
75
+ # self.after_save()
76
+
77
+ return self
78
+
79
+ # except IntegrityError:
80
+ # log.quiet(f"{self} already exists in the database.")
81
+ # session.rollback()
82
+
83
+ # TODO shouldn't this be handled by pydantic?
84
+ def json(self, **kwargs):
85
+ return json.dumps(self.dict(), default=str, **kwargs)
86
+
87
+ @classmethod
88
+ def count(cls):
89
+ """
90
+ Returns the number of records in the database.
91
+ """
92
+ # TODO should move this to the wrapper
93
+ with get_session() as session:
94
+ query = sql.select(sql.func.count()).select_from(cls)
95
+ return session.exec(query).one()
96
+
97
+ # TODO what's super dangerous here is you pass a kwarg which does not map to a specific
98
+ # field it will result in `True`, which will return all records, and not give you any typing
99
+ # errors. Dangerous when iterating on structure quickly
100
+ # TODO can we pass the generic of the superclass in?
101
+ @classmethod
102
+ def get(cls, *args: sa.BinaryExpression, **kwargs: t.Any):
103
+ """
104
+ Gets a single record from the database. Pass an PK ID or a kwarg to filter by.
105
+ """
106
+
107
+ # special case for getting by ID
108
+ if len(args) == 1 and isinstance(args[0], int):
109
+ # TODO id is hardcoded, not good! Need to dynamically pick the best uid field
110
+ kwargs["id"] = args[0]
111
+ args = []
112
+
113
+ statement = sql.select(cls).filter(*args).filter_by(**kwargs)
114
+ with get_session() as session:
115
+ return session.exec(statement).first()
116
+
117
+ @classmethod
118
+ def all(cls):
119
+ with get_session() as session:
120
+ results = session.exec(sql.select(cls))
121
+
122
+ # TODO do we need this or can we just return results?
123
+ for result in results:
124
+ yield result
125
+
126
+ @classmethod
127
+ def sample(cls):
128
+ """
129
+ Pick a random record from the database.
130
+
131
+ Helpful for testing and console debugging.
132
+ """
133
+
134
+ query = sql.select(cls).order_by(sql.func.random()).limit(1)
135
+
136
+ with get_session() as session:
137
+ return session.exec(query).one()
@@ -0,0 +1,84 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from sqlmodel.sql.expression import SelectOfScalar
4
+
5
+ WrappedModelType = TypeVar("WrappedModelType")
6
+
7
+
8
+ def compile_sql(target: SelectOfScalar):
9
+ return str(target.compile(get_engine().connect()))
10
+
11
+
12
+ class QueryWrapper(Generic[WrappedModelType]):
13
+ """
14
+ Make it easy to run queries off of a model
15
+ """
16
+
17
+ def __init__(self, cls, *args) -> None:
18
+ # TODO add generics here
19
+ # self.target: SelectOfScalar[T] = sql.select(cls)
20
+
21
+ if args:
22
+ # very naive, let's assume the args are specific select statements
23
+ self.target = sql.select(*args).select_from(cls)
24
+ else:
25
+ self.target = sql.select(cls)
26
+
27
+ # TODO the .exec results should be handled in one shot
28
+
29
+ def first(self):
30
+ with get_session() as session:
31
+ return session.exec(self.target).first()
32
+
33
+ def one(self):
34
+ with get_session() as session:
35
+ return session.exec(self.target).one()
36
+
37
+ def all(self):
38
+ with get_session() as session:
39
+ result = session.exec(self.target)
40
+ for row in result:
41
+ yield row
42
+
43
+ def exec(self):
44
+ # TODO do we really need a unique session each time?
45
+ with get_session() as session:
46
+ return session.exec(self.target)
47
+
48
+ def delete(self):
49
+ with get_session() as session:
50
+ session.delete(self.target)
51
+
52
+ def __getattr__(self, name):
53
+ """
54
+ This is called to retrieve the function to execute
55
+ """
56
+
57
+ # TODO prefer methods defined in this class
58
+ if not hasattr(self.target, name):
59
+ return super().__getattribute__(name)
60
+
61
+ attr = getattr(self.target, name)
62
+
63
+ if callable(attr):
64
+
65
+ def wrapper(*args, **kwargs):
66
+ result = attr(*args, **kwargs)
67
+ self.target = result
68
+ return self
69
+
70
+ return wrapper
71
+
72
+ # If the attribute or method is not defined in this class,
73
+ # delegate the call to the `target` object
74
+ return getattr(self.target, name)
75
+
76
+ def sql(self):
77
+ """
78
+ Output the raw SQL of the query
79
+ """
80
+
81
+ return compile_sql(self.target)
82
+
83
+ def __repr__(self) -> str:
84
+ return f"{self.__class__.__name__}: Current SQL:\n{self.sql()}"
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+
3
+ import sqlalchemy as sa
4
+
5
+ # TODO raw sql https://github.com/tiangolo/sqlmodel/discussions/772
6
+ from sqlmodel import Field
7
+
8
+ # @classmethod
9
+ # def select(cls):
10
+ # with get_session() as session:
11
+ # results = session.exec(sql.select(cls))
12
+
13
+ # for result in results:
14
+ # yield result
15
+
16
+
17
+ class TimestampMixin:
18
+ """
19
+ Simple created at and updated at timestamps. Mix them into your model:
20
+
21
+ >>> class MyModel(TimestampMixin, SQLModel):
22
+ >>> pass
23
+
24
+ Originally pulled from: https://github.com/tiangolo/sqlmodel/issues/252
25
+ """
26
+
27
+ created_at: datetime | None = Field(
28
+ default=None,
29
+ sa_type=sa.DateTime(timezone=True),
30
+ sa_column_kwargs={"server_default": sa.func.now()},
31
+ nullable=False,
32
+ )
33
+
34
+ updated_at: datetime | None = Field(
35
+ default=None,
36
+ sa_type=sa.DateTime(timezone=True),
37
+ sa_column_kwargs={"onupdate": sa.func.now(), "server_default": sa.func.now()},
38
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Michael Bianco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: activemodel
3
+ Version: 0.3.0
4
+ Summary: Make SQLModel more like an a real ORM
5
+ Author-email: Michael Bianco <iloveitaly@gmail.com>
6
+ Project-URL: Repository, https://github.com/iloveitaly/activemodel
7
+ Keywords: sqlmodel,orm,activerecord,activemodel,sqlalchemy
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: pydash>=8.0.4
12
+ Requires-Dist: sqlmodel>=0.0.22
13
+
14
+ # ActiveModel: ORM Wrapper for SQLModel
15
+
16
+ No, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.
17
+
18
+ SQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.
19
+
20
+ This package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:
21
+
22
+ * Timestamp column mixins
23
+ * Lifecycle hooks
24
+
25
+ ## Related Projects
26
+
27
+ * https://github.com/woofz/sqlmodel-basecrud
28
+
29
+ ## Inspiration
30
+
31
+ * https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg
32
+ * [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)
33
+ * https://github.com/fastapiutils/fastapi-utils/
34
+ *
@@ -0,0 +1,10 @@
1
+ activemodel/__init__.py,sha256=WWLMytAi2VO3-HdySf4IpbeSCPMlVVO_GGQlQNA6UWU,216
2
+ activemodel/base_model.py,sha256=fk_g6jC1GCkAFo78UYjTONyFE8iYQH1bfvxdWG-_178,4200
3
+ activemodel/query_wrapper.py,sha256=JiYN8EN9_gXpKrFw348gs4QixwtrEN50fJF-3uv5Gmg,2319
4
+ activemodel/timestamps.py,sha256=8odUxQ1c0OouPAVioMTkD277w6S28Pk17pwCsaxKgww,991
5
+ activemodel-0.3.0.dist-info/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
6
+ activemodel-0.3.0.dist-info/METADATA,sha256=PrnCUuQVLSuCQ-tbwEMzHXrqMHq9MHTNYVgvRh9uXng,1174
7
+ activemodel-0.3.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
8
+ activemodel-0.3.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
9
+ activemodel-0.3.0.dist-info/top_level.txt,sha256=JCMUN_seFIi6GXtnTQRWfxXDx6Oj1uok8qapQWbWKDM,12
10
+ activemodel-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ activemodel = python_package_template:main
@@ -0,0 +1 @@
1
+ activemodel