activemodel 0.9.0__py3-none-any.whl → 0.11.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.
activemodel/base_model.py CHANGED
@@ -4,11 +4,11 @@ from uuid import UUID
4
4
 
5
5
  import pydash
6
6
  import sqlalchemy as sa
7
- from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
8
- from sqlalchemy.orm.base import instance_state
9
7
  import sqlmodel as sm
10
8
  from sqlalchemy import Connection, event
11
9
  from sqlalchemy.orm import Mapper, declared_attr
10
+ from sqlalchemy.orm.attributes import flag_modified as sa_flag_modified
11
+ from sqlalchemy.orm.base import instance_state
12
12
  from sqlmodel import Column, Field, Session, SQLModel, inspect, select
13
13
  from typeid import TypeID
14
14
 
@@ -19,6 +19,7 @@ from . import get_column_from_field_patch # noqa: F401
19
19
  from .logger import logger
20
20
  from .query_wrapper import QueryWrapper
21
21
  from .session_manager import get_session
22
+ from sqlalchemy.dialects.postgresql import insert as postgres_insert
22
23
 
23
24
  POSTGRES_INDEXES_NAMING_CONVENTION = {
24
25
  "ix": "%(column_0_label)s_idx",
@@ -137,8 +138,15 @@ class BaseModel(SQLModel):
137
138
  cls.__table_args__ = {"comment": doc}
138
139
  elif isinstance(table_args, dict):
139
140
  table_args.setdefault("comment", doc)
141
+ elif isinstance(table_args, tuple):
142
+ # If it's a tuple, we need to convert it to a list and add the comment
143
+ table_args = list(table_args)
144
+ table_args.append({"comment": doc})
145
+ cls.__table_args__ = tuple(table_args)
140
146
  else:
141
- raise ValueError("Unexpected __table_args__ type")
147
+ raise ValueError(
148
+ f"Unexpected __table_args__ type {type(table_args)}, expected dictionary."
149
+ )
142
150
 
143
151
  # TODO no type check decorator here
144
152
  @declared_attr
@@ -163,8 +171,10 @@ class BaseModel(SQLModel):
163
171
  """
164
172
  Returns a `Field` object referencing the foreign key of the model.
165
173
 
166
- >>> other_model_id: int
167
- >>> other_model = OtherModel.foreign_key()
174
+ Helps quickly build a many-to-one or one-to-one relationship.
175
+
176
+ >>> other_model_id: int = OtherModel.foreign_key()
177
+ >>> other_model = Relationship()
168
178
  """
169
179
 
170
180
  field_options = {"nullable": False} | kwargs
@@ -186,6 +196,43 @@ class BaseModel(SQLModel):
186
196
  "convenience method to avoid having to write .select().where() in order to add conditions"
187
197
  return cls.select().where(*args)
188
198
 
199
+ # TODO we should add an instance method for this as well
200
+ @classmethod
201
+ def upsert(
202
+ cls,
203
+ data: dict[str, t.Any],
204
+ unique_by: str | list[str],
205
+ ) -> t.Self:
206
+ """
207
+ This method will insert a new record if it doesn't exist, or update the existing record if it does.
208
+
209
+ It uses SQLAlchemy's `on_conflict_do_update` and does not yet support MySQL. Some implementation details below.
210
+
211
+ ---
212
+
213
+ - `index_elements=["name"]`: Specifies the column(s) to check for conflicts (e.g., unique constraint or index). If a row with the same "name" exists, it triggers the update instead of an insert.
214
+ - `values`: Defines the data to insert (e.g., `name="example", value=123`). If no conflict occurs, this data is inserted as a new row.
215
+
216
+ The `set_` parameter (e.g., `set_=dict(value=123)`) then dictates what gets updated on conflict, overriding matching fields in `values` if specified.
217
+ """
218
+ index_elements = [unique_by] if isinstance(unique_by, str) else unique_by
219
+
220
+ stmt = (
221
+ postgres_insert(cls)
222
+ .values(**data)
223
+ .on_conflict_do_update(index_elements=index_elements, set_=data)
224
+ .returning(cls)
225
+ )
226
+
227
+ with get_session() as session:
228
+ result = session.exec(stmt)
229
+ session.commit()
230
+
231
+ # TODO this is so ugly:
232
+ result = result.one()[0]
233
+
234
+ return result
235
+
189
236
  def delete(self):
190
237
  with get_session() as session:
191
238
  if old_session := Session.object_session(self):
@@ -256,9 +303,9 @@ class BaseModel(SQLModel):
256
303
  def is_new(self) -> bool:
257
304
  return not self._sa_instance_state.has_identity
258
305
 
259
- def flag_modified(self, *args: str):
306
+ def flag_modified(self, *args: str) -> None:
260
307
  """
261
- Flag one or more fields as modified. Useful for marking a field containing sub-objects as modified.
308
+ Flag one or more fields as modified/mutated/dirty. Useful for marking a field containing sub-objects as modified.
262
309
 
263
310
  Will throw an error if an invalid field is passed.
264
311
  """
@@ -1,46 +1,41 @@
1
1
  from sqlmodel import Column, Field
2
2
  from typeid import TypeID
3
3
 
4
+ from activemodel.types import typeid_patch # noqa: F401
4
5
  from activemodel.types.typeid import TypeIDType
5
6
 
6
7
  # global list of prefixes to ensure uniqueness
7
- _prefixes = []
8
+ _prefixes: list[str] = []
8
9
 
9
10
 
10
11
  def TypeIDMixin(prefix: str):
12
+ """
13
+ Mixin that adds a TypeID primary key field to a SQLModel. Specify the prefix to use for the TypeID.
14
+ """
15
+
16
+ # make sure duplicate prefixes are not used!
17
+ # NOTE this will cause issues on code reloads
11
18
  assert prefix
12
19
  assert prefix not in _prefixes, (
13
20
  f"prefix {prefix} already exists, pick a different one"
14
21
  )
15
22
 
16
23
  class _TypeIDMixin:
24
+ __abstract__ = True
25
+
17
26
  id: TypeIDType = Field(
18
- sa_column=Column(TypeIDType(prefix), primary_key=True, nullable=False),
19
- default_factory=lambda: TypeID(prefix),
27
+ sa_column=Column(
28
+ TypeIDType(prefix),
29
+ primary_key=True,
30
+ nullable=False,
31
+ # default on the sa_column level ensures that an ID is generated when creating a new record, even when
32
+ # raw SQLAlchemy operations are used instead of activemodel operations
33
+ default=lambda: TypeID(prefix),
34
+ ),
35
+ # add a database comment to document the prefix, since it's not stored in the DB otherwise
36
+ description=f"TypeID with prefix: {prefix}",
20
37
  )
21
38
 
22
39
  _prefixes.append(prefix)
23
40
 
24
41
  return _TypeIDMixin
25
-
26
-
27
- # TODO not sure if I love the idea of a dynamic class for each mixin as used above
28
- # may give this approach another shot in the future
29
- # class TypeIDMixin2:
30
- # """
31
- # Mixin class that adds a TypeID primary key to models.
32
-
33
-
34
- # >>> class MyModel(BaseModel, TypeIDMixin, prefix="xyz", table=True):
35
- # >>> name: str
36
-
37
- # Will automatically have an `id` field with prefix "xyz"
38
- # """
39
-
40
- # def __init_subclass__(cls, *, prefix: str, **kwargs):
41
- # super().__init_subclass__(**kwargs)
42
-
43
- # cls.id: uuid.UUID = Field(
44
- # sa_column=Column(TypeIDType(prefix), primary_key=True),
45
- # default_factory=lambda: TypeID(prefix),
46
- # )
@@ -47,7 +47,7 @@ class SessionManager:
47
47
  "singleton instance of SessionManager"
48
48
 
49
49
  session_connection: Connection | None
50
- "optionally specify a specific session connection to use for all get_session() calls, useful for testing"
50
+ "optionally specify a specific session connection to use for all get_session() calls, useful for testing and migrations"
51
51
 
52
52
  @classmethod
53
53
  def get_instance(cls, database_url: str | None = None) -> "SessionManager":
@@ -72,6 +72,7 @@ class SessionManager:
72
72
  self._database_url,
73
73
  # NOTE very important! This enables pydantic models to be serialized for JSONB columns
74
74
  json_serializer=_serialize_pydantic_model,
75
+ # TODO move to a constants area
75
76
  echo=config("ACTIVEMODEL_LOG_SQL", cast=bool, default=False),
76
77
  # https://docs.sqlalchemy.org/en/20/core/pooling.html#disconnect-handling-pessimistic
77
78
  pool_pre_ping=True,
@@ -81,6 +82,8 @@ class SessionManager:
81
82
  return self._engine
82
83
 
83
84
  def get_session(self):
85
+ "get a new database session, respecting any globally set sessions"
86
+
84
87
  if gsession := _session_context.get():
85
88
 
86
89
  @contextlib.contextmanager
@@ -102,23 +105,40 @@ def init(database_url: str):
102
105
 
103
106
 
104
107
  def get_engine():
108
+ "alias to get the database engine without importing SessionManager"
105
109
  return SessionManager.get_instance().get_engine()
106
110
 
107
111
 
108
112
  def get_session():
113
+ "alias to get a database session without importing SessionManager"
109
114
  return SessionManager.get_instance().get_session()
110
115
 
111
116
 
112
- # contextvars must be at the top-level of a module! You will not get a warning if you don't do this.
113
- # ContextVar is implemented in C, so it's very special and is both thread-safe and asyncio safe. This variable gives us
114
- # a place to persist a session to use globally across the application.
115
117
  _session_context = contextvars.ContextVar[Session | None](
116
118
  "session_context", default=None
117
119
  )
120
+ """
121
+ This is a VERY important ContextVar, it sets a global session to be used across all ActiveModel operations by default
122
+ and ensures get_session() uses this session as well.
123
+
124
+ contextvars must be at the top-level of a module! You will not get a warning if you don't do this.
125
+ ContextVar is implemented in C, so it's very special and is both thread-safe and asyncio safe. This variable gives us
126
+ a place to persist a session to use globally across the application.
127
+ """
118
128
 
119
129
 
120
130
  @contextlib.contextmanager
121
131
  def global_session():
132
+ """
133
+ Generate a session shared across all activemodel calls.
134
+
135
+ Alternatively, you can pass a session to use globally into the context manager, which is helpful for migrations
136
+ and testing.
137
+ """
138
+
139
+ if _session_context.get() is not None:
140
+ raise RuntimeError("global session already set")
141
+
122
142
  with SessionManager.get_instance().get_session() as s:
123
143
  token = _session_context.set(s)
124
144
 
@@ -140,6 +160,9 @@ async def aglobal_session():
140
160
  >>> )
141
161
  """
142
162
 
163
+ if _session_context.get() is not None:
164
+ raise RuntimeError("global session already set")
165
+
143
166
  with SessionManager.get_instance().get_session() as s:
144
167
  token = _session_context.set(s)
145
168
 
@@ -2,7 +2,6 @@
2
2
  Lifted from: https://github.com/akhundMurad/typeid-python/blob/main/examples/sqlalchemy.py
3
3
  """
4
4
 
5
- from typing import Optional
6
5
  from uuid import UUID
7
6
 
8
7
  from pydantic import (
@@ -19,25 +18,32 @@ from activemodel.errors import TypeIDValidationError
19
18
  class TypeIDType(types.TypeDecorator):
20
19
  """
21
20
  A SQLAlchemy TypeDecorator that allows storing TypeIDs in the database.
22
- The prefix will not be persisted, instead the database-native UUID field will be used.
23
- At retrieval time a TypeID will be constructed based on the configured prefix and the
24
- UUID value from the database.
25
-
26
- Usage:
27
- # will result in TypeIDs such as "user_01h45ytscbebyvny4gc8cr8ma2"
28
- id = mapped_column(
29
- TypeIDType("user"),
30
- primary_key=True,
31
- default=lambda: TypeID("user")
32
- )
21
+
22
+ The prefix will not be persisted to the database, instead the database-native UUID field will be used.
23
+ At retrieval time a TypeID will be constructed (in python) based on the configured prefix and the UUID
24
+ value from the database.
25
+
26
+ For example:
27
+
28
+ >>> id = mapped_column(
29
+ >>> TypeIDType("user"),
30
+ >>> primary_key=True,
31
+ >>> default=lambda: TypeID("user")
32
+ >>> )
33
+
34
+ Will result in TypeIDs such as "user_01h45ytscbebyvny4gc8cr8ma2". There's a mixin provided to make it easy
35
+ to add a `id` pk field to your model with a specific prefix.
33
36
  """
34
37
 
38
+ # TODO are we sure we wouldn't use TypeID here?
35
39
  impl = types.Uuid
40
+ # TODO why the types version?
36
41
  # impl = uuid.UUID
42
+
37
43
  cache_ok = True
38
- prefix: Optional[str] = None
44
+ prefix: str
39
45
 
40
- def __init__(self, prefix: Optional[str], *args, **kwargs):
46
+ def __init__(self, prefix: str, *args, **kwargs):
41
47
  self.prefix = prefix
42
48
  super().__init__(*args, **kwargs)
43
49
 
@@ -90,6 +96,8 @@ class TypeIDType(types.TypeDecorator):
90
96
  raise ValueError("Unexpected input type")
91
97
 
92
98
  def process_result_value(self, value, dialect):
99
+ "convert a raw UUID, without a prefix, to a TypeID with the correct prefix"
100
+
93
101
  if value is None:
94
102
  return None
95
103
 
@@ -123,13 +131,19 @@ class TypeIDType(types.TypeDecorator):
123
131
  - https://github.com/alice-biometrics/petisco/blob/b01ef1b84949d156f73919e126ed77aa8e0b48dd/petisco/base/domain/model/uuid.py#L50
124
132
  """
125
133
 
134
+ def convert_from_string(value: str | TypeID) -> TypeID:
135
+ if isinstance(value, TypeID):
136
+ return value
137
+
138
+ return TypeID.from_string(value)
139
+
126
140
  from_uuid_schema = core_schema.chain_schema(
127
141
  [
128
142
  # TODO not sure how this is different from the UUID schema, should try it out.
129
143
  # core_schema.is_instance_schema(TypeID),
130
144
  # core_schema.uuid_schema(),
131
145
  core_schema.no_info_plain_validator_function(
132
- TypeID.from_string,
146
+ convert_from_string,
133
147
  json_schema_input_schema=core_schema.str_schema(),
134
148
  ),
135
149
  ]
@@ -151,11 +165,10 @@ class TypeIDType(types.TypeDecorator):
151
165
  # )
152
166
  # },
153
167
  python_schema=core_schema.union_schema([from_uuid_schema]),
154
- serialization=core_schema.plain_serializer_function_ser_schema(
155
- lambda x: str(x)
156
- ),
168
+ serialization=core_schema.plain_serializer_function_ser_schema(str),
157
169
  )
158
170
 
171
+ # TODO I have a feeling that the `serialization` param in the above method solves this for us.
159
172
  @classmethod
160
173
  def __get_pydantic_json_schema__(
161
174
  cls, schema: CoreSchema, handler: GetJsonSchemaHandler
@@ -164,18 +177,18 @@ class TypeIDType(types.TypeDecorator):
164
177
  Called when generating the openapi schema. This overrides the `function-plain` type which
165
178
  is generated by the `no_info_plain_validator_function`.
166
179
 
167
- This logis seems to be a hot part of the codebase, so I'd expect this to break as pydantic
180
+ This logic seems to be a hot part of the codebase, so I'd expect this to break as pydantic
168
181
  fastapi continue to evolve.
169
182
 
170
183
  Note that this method can return multiple types. A return value can be as simple as:
171
184
 
172
- {"type": "string"}
185
+ >>> {"type": "string"}
173
186
 
174
187
  Or, you could return a more specific JSON schema type:
175
188
 
176
- core_schema.uuid_schema()
189
+ >>> core_schema.uuid_schema()
177
190
 
178
- The problem with using something like uuid_schema is the specifi patterns
191
+ The problem with using something like uuid_schema is the specific patterns
179
192
 
180
193
  https://github.com/BeanieODM/beanie/blob/2190cd9d1fc047af477d5e6897cc283799f54064/beanie/odm/fields.py#L153
181
194
  """
@@ -0,0 +1,22 @@
1
+ from typing import Any, Type
2
+
3
+ from pydantic import GetCoreSchemaHandler
4
+ from pydantic_core import CoreSchema, core_schema
5
+
6
+ from typeid import TypeID
7
+
8
+
9
+ @classmethod
10
+ def get_pydantic_core_schema(
11
+ cls: Type[TypeID], source_type: Any, handler: GetCoreSchemaHandler
12
+ ) -> CoreSchema:
13
+ return core_schema.union_schema(
14
+ [
15
+ core_schema.str_schema(),
16
+ core_schema.is_instance_schema(cls),
17
+ ],
18
+ serialization=core_schema.plain_serializer_function_ser_schema(str),
19
+ )
20
+
21
+
22
+ TypeID.__get_pydantic_core_schema__ = get_pydantic_core_schema
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activemodel
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: Make SQLModel more like an a real ORM
5
5
  Project-URL: Repository, https://github.com/iloveitaly/activemodel
6
6
  Author-email: Michael Bianco <iloveitaly@gmail.com>
@@ -110,14 +110,14 @@ index 0d07420..a63631c 100644
110
110
  # Use forward slashes (/) also on windows to provide an os agnostic path
111
111
  -script_location = .
112
112
  +script_location = migrations
113
-
113
+
114
114
  # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
115
115
  # Uncomment the line below if you want the files to be prepended with date and time
116
116
  # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
117
117
  # for all available tokens
118
118
  # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
119
119
  +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
120
-
120
+
121
121
  # sys.path path, will be prepended to sys.path if present.
122
122
  # defaults to the current working directory.
123
123
  diff --git i/test/migrations/env.py w/test/migrations/env.py
@@ -129,12 +129,12 @@ index 36112a3..a1e15c2 100644
129
129
  +# isort: off
130
130
  +
131
131
  from logging.config import fileConfig
132
-
132
+
133
133
  from sqlalchemy import engine_from_config
134
134
  @@ -14,11 +17,17 @@ config = context.config
135
135
  if config.config_file_name is not None:
136
136
  fileConfig(config.config_file_name)
137
-
137
+
138
138
  +from sqlmodel import SQLModel
139
139
  +from test.models import *
140
140
  +from test.utils import database_url
@@ -147,7 +147,7 @@ index 36112a3..a1e15c2 100644
147
147
  # target_metadata = mymodel.Base.metadata
148
148
  -target_metadata = None
149
149
  +target_metadata = SQLModel.metadata
150
-
150
+
151
151
  # other values from the config, defined by the needs of env.py,
152
152
  # can be acquired:
153
153
  diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
@@ -155,13 +155,13 @@ index fbc4b07..9dc78bb 100644
155
155
  --- i/test/migrations/script.py.mako
156
156
  +++ w/test/migrations/script.py.mako
157
157
  @@ -9,6 +9,8 @@ from typing import Sequence, Union
158
-
158
+
159
159
  from alembic import op
160
160
  import sqlalchemy as sa
161
161
  +import sqlmodel
162
162
  +import activemodel
163
163
  ${imports if imports else ""}
164
-
164
+
165
165
  # revision identifiers, used by Alembic.
166
166
  ```
167
167
 
@@ -178,7 +178,7 @@ This tool is added to all `BaseModel`s and makes it easy to write SQL queries. S
178
178
 
179
179
  ### Easy Database Sessions
180
180
 
181
- I hate the idea f
181
+ I hate the idea f
182
182
 
183
183
  * Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
184
184
  * Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.
@@ -186,7 +186,7 @@ I hate the idea f
186
186
  There are a couple of thorny problems we need to solve for here:
187
187
 
188
188
  * In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.
189
- *
189
+ *
190
190
 
191
191
  https://github.com/tomwojcik/starlette-context
192
192
 
@@ -202,8 +202,11 @@ https://github.com/tomwojcik/starlette-context
202
202
  SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
203
203
 
204
204
  * `__sqlmodel_relationships__` is where any `RelationshipInfo` objects are stored. This is used to generate relationship fields on the object.
205
- * `ModelClass.relationship_name.property.local_columns`
205
+ * `ModelClass.relationship_name.property.local_columns`
206
206
  * Get cached fields from a model `object_state(instance).dict.get(field_name)`
207
+ * Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
208
+ * Is a model dirty `instance_state(instance).modified`
209
+ * `select(Table).outerjoin??` won't work in a ipython session, but `Table.__table__.outerjoin??` will. `__table__` is a reference to the underlying SQLAlchemy table record.
207
210
 
208
211
  ### TypeID
209
212
 
@@ -1,24 +1,25 @@
1
1
  activemodel/__init__.py,sha256=q_lHQyIM70ApvjduTo9GtenQjJXsfYZsAAquD_51kF4,137
2
- activemodel/base_model.py,sha256=Xn5RCN-tAmGGkhaH8gnT2PFIkbefeSv85bPP41cJp14,14715
2
+ activemodel/base_model.py,sha256=2lUcmOxS1i1K9qdKEVjI9vLpbFltUlh-cAfvGPYbXlI,16709
3
3
  activemodel/celery.py,sha256=L1vKcO_HoPA5ZCfsXjxgPpDUMYDuoQMakGA9rppN7Lo,897
4
4
  activemodel/errors.py,sha256=wycWYmk9ws4TZpxvTdtXVy2SFESb8NqKgzdivBoF0vw,115
5
5
  activemodel/get_column_from_field_patch.py,sha256=wAEDm_ZvSqyJwfgkXVpxsevw11hd-7VLy7zuJG8Ak7Y,4986
6
6
  activemodel/logger.py,sha256=vU7QiGSy_AJuJFmClUocqIJ-Ltku_8C24ZU8L6fLJR0,53
7
7
  activemodel/query_wrapper.py,sha256=rNdvueppMse2MIi-RafTEC34GPGRal_wqH2CzhmlWS8,2520
8
- activemodel/session_manager.py,sha256=Vtg8Lf8vUNPegdRW-fyE-Ng5wtN3hTMfUezdFUiJ1fs,4585
8
+ activemodel/session_manager.py,sha256=9Yb5sPOUginIC7M0oB3dvxkhaidX2iKGFpV3lpXkKzw,5454
9
9
  activemodel/utils.py,sha256=g17UqkphzTmb6YdpmYwT1TM00eDiXXuWn39-xNiu0AA,2112
10
10
  activemodel/mixins/__init__.py,sha256=05EQl2u_Wgf_wkly-GTaTsR7zWpmpKcb96Js7r_rZTw,160
11
11
  activemodel/mixins/pydantic_json.py,sha256=0pprGZA95BGZL4WOh--NJcvxLWey4YW85lLk4GGTjFM,3530
12
12
  activemodel/mixins/soft_delete.py,sha256=Ax4mGsQI7AVTE8c4GiWxpyB_W179-dDct79GtjP0owU,461
13
13
  activemodel/mixins/timestamps.py,sha256=Q-IFljeVVJQqw3XHdOi7dkqzefiVg1zhJvq_bldpmjg,992
14
- activemodel/mixins/typeid.py,sha256=DGjlIg8PRBYoaBbWkkxc6jkScyl-p53KuSR98lLgAvE,1284
14
+ activemodel/mixins/typeid.py,sha256=VjhORJ-wf3HT43DMmez6MmZTWjH7fb5c-7Qcdwgdiqg,1331
15
15
  activemodel/pytest/__init__.py,sha256=W9KKQHbPkyq0jrMXaiL8hG2Nsbjy_LN9HhvgGm8W_7g,98
16
16
  activemodel/pytest/transaction.py,sha256=ln-3N5tXHT0fqy6a8m_NIYg5AXAeA2hDuftQtFxNqi4,2600
17
17
  activemodel/pytest/truncate.py,sha256=IGiPLkUm2yyOKww6c6CKcVbwi2xAAFBopx9q2ABfu8w,1582
18
18
  activemodel/types/__init__.py,sha256=y5fiGVtPJxGEhuf-TvyrkhM2yaKRcIWo6XAx-CFFjM8,31
19
- activemodel/types/typeid.py,sha256=1xB79DGIC5-P-PcLpeZW9Ed_WjFOmmVW1yl2Q3pPJis,7250
20
- activemodel-0.9.0.dist-info/METADATA,sha256=AWt9ERLLgE1X1aQvJSuPwDTBmqoH9EW2_zglUlJVfPY,9651
21
- activemodel-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- activemodel-0.9.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
23
- activemodel-0.9.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
24
- activemodel-0.9.0.dist-info/RECORD,,
19
+ activemodel/types/typeid.py,sha256=Vqzete8IvZ5SHKf3DW2eKIWxweIZvUN2kjhLNuOl3Cc,7753
20
+ activemodel/types/typeid_patch.py,sha256=y6kiCJQ_NzeKfuI4UtRAs7QW_nEog5RIA_-k4HUBMkU,575
21
+ activemodel-0.11.0.dist-info/METADATA,sha256=tNJjq1XJNFXkua5Ui7XCr6DR_qtSQvbK_f_UPtCwHTE,9986
22
+ activemodel-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ activemodel-0.11.0.dist-info/entry_points.txt,sha256=YLX62TP_hR-n3HMBkdBex4W7XRiyOtIPkwy22puIjjQ,61
24
+ activemodel-0.11.0.dist-info/licenses/LICENSE,sha256=L8mmpX47rB-xtJ_HsK0zpfO6viEjxbLYGn70BMp8os4,1071
25
+ activemodel-0.11.0.dist-info/RECORD,,