plain.models 0.40.0__py3-none-any.whl → 0.41.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.
plain/models/AGENTS.md ADDED
@@ -0,0 +1,4 @@
1
+ # Plain Models AGENTS.md
2
+
3
+ - Use the `plain makemigrations` command to create new migrations. Only write migrations by hand if they are custom data migrations.
4
+ - Use `plain migrate --backup` to run migrations.
plain/models/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.41.0](https://github.com/dropseed/plain/releases/plain-models@0.41.0) (2025-09-09)
4
+
5
+ ### What's changed
6
+
7
+ - Python 3.13 is now the minimum required version ([d86e307](https://github.com/dropseed/plain/commit/d86e307))
8
+ - Removed the `earliest()`, `latest()`, and `get_latest_by` model meta option - use `order_by().first()` and `order_by().last()` instead ([b6093a8](https://github.com/dropseed/plain/commit/b6093a8))
9
+ - Removed automatic ordering in `first()` and `last()` queryset methods - they now respect the existing queryset ordering without adding default ordering ([adc19a6](https://github.com/dropseed/plain/commit/adc19a6))
10
+ - Added code location attributes to database operation tracing, showing the source file, line number, and function where the query originated ([da36a17](https://github.com/dropseed/plain/commit/da36a17))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - Replace usage of `earliest()`, `latest()`, and model `Meta` `get_latest_by` queryset methods with equivalent `order_by().first()` or `order_by().last()` calls
15
+ - The `first()` and `last()` methods no longer automatically add ordering by `id` - explicitly add `.order_by()` to your querysets or `ordering` to your models `Meta` class if needed
16
+
17
+ ## [0.40.1](https://github.com/dropseed/plain/releases/plain-models@0.40.1) (2025-09-03)
18
+
19
+ ### What's changed
20
+
21
+ - Internal documentation updates for agent commands ([df3edbf0bd](https://github.com/dropseed/plain/commit/df3edbf0bd))
22
+
23
+ ### Upgrade instructions
24
+
25
+ - No changes required
26
+
3
27
  ## [0.40.0](https://github.com/dropseed/plain/releases/plain-models@0.40.0) (2025-08-05)
4
28
 
5
29
  ### What's changed
@@ -450,7 +450,6 @@ class AlterModelOptions(ModelOptionOperation):
450
450
  "base_manager_name",
451
451
  "default_manager_name",
452
452
  "default_related_name",
453
- "get_latest_by",
454
453
  "ordering",
455
454
  ]
456
455
 
plain/models/options.py CHANGED
@@ -25,7 +25,6 @@ DEFAULT_NAMES = (
25
25
  "db_table",
26
26
  "db_table_comment",
27
27
  "ordering",
28
- "get_latest_by",
29
28
  "package_label",
30
29
  "models_registry",
31
30
  "default_related_name",
@@ -74,7 +73,6 @@ class Options:
74
73
  self.constraints = []
75
74
  self.object_name = None
76
75
  self.package_label = package_label
77
- self.get_latest_by = None
78
76
  self.required_db_features = []
79
77
  self.required_db_vendor = None
80
78
  self.meta = meta
plain/models/otel.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import traceback
2
3
  from contextlib import contextmanager
3
4
  from typing import Any
4
5
 
@@ -8,6 +9,13 @@ from opentelemetry.semconv._incubating.attributes.db_attributes import (
8
9
  DB_QUERY_PARAMETER_TEMPLATE,
9
10
  DB_USER,
10
11
  )
12
+ from opentelemetry.semconv.attributes.code_attributes import (
13
+ CODE_COLUMN_NUMBER,
14
+ CODE_FILE_PATH,
15
+ CODE_FUNCTION_NAME,
16
+ CODE_LINE_NUMBER,
17
+ CODE_STACKTRACE,
18
+ )
11
19
  from opentelemetry.semconv.attributes.db_attributes import (
12
20
  DB_COLLECTION_NAME,
13
21
  DB_NAMESPACE,
@@ -126,6 +134,8 @@ def db_span(db, sql: Any, *, many: bool = False, params=None):
126
134
  DB_OPERATION_NAME: operation,
127
135
  }
128
136
 
137
+ attrs.update(_get_code_attributes())
138
+
129
139
  # Add collection name if detected
130
140
  if collection_name:
131
141
  attrs[DB_COLLECTION_NAME] = collection_name
@@ -173,3 +183,43 @@ def suppress_db_tracing():
173
183
  yield
174
184
  finally:
175
185
  otel_context.detach(token)
186
+
187
+
188
+ def _get_code_attributes():
189
+ """Extract code context attributes for the current database query.
190
+
191
+ Returns a dict of OpenTelemetry code attributes.
192
+ """
193
+ stack = traceback.extract_stack()
194
+
195
+ # Find the user code frame
196
+ for frame in reversed(stack):
197
+ filepath = frame.filename
198
+ if not filepath:
199
+ continue
200
+
201
+ if "/plain/models/" in filepath:
202
+ continue
203
+
204
+ if filepath.endswith("contextlib.py"):
205
+ continue
206
+
207
+ # Found user code - build attributes dict
208
+ attrs = {}
209
+
210
+ if filepath:
211
+ attrs[CODE_FILE_PATH] = filepath
212
+ if frame.lineno:
213
+ attrs[CODE_LINE_NUMBER] = frame.lineno
214
+ if frame.name:
215
+ attrs[CODE_FUNCTION_NAME] = frame.name
216
+ if frame.colno:
217
+ attrs[CODE_COLUMN_NUMBER] = frame.colno
218
+
219
+ # Add full stack trace only in DEBUG mode (expensive)
220
+ if settings.DEBUG:
221
+ attrs[CODE_STACKTRACE] = "".join(traceback.format_stack())
222
+
223
+ return attrs
224
+
225
+ return {}
plain/models/query.py CHANGED
@@ -873,59 +873,14 @@ class QuerySet:
873
873
  )
874
874
  return params
875
875
 
876
- def _earliest(self, *fields):
877
- """
878
- Return the earliest object according to fields (if given) or by the
879
- model's Meta.get_latest_by.
880
- """
881
- if fields:
882
- order_by = fields
883
- else:
884
- order_by = getattr(self.model._meta, "get_latest_by")
885
- if order_by and not isinstance(order_by, tuple | list):
886
- order_by = (order_by,)
887
- if order_by is None:
888
- raise ValueError(
889
- "earliest() and latest() require either fields as positional "
890
- "arguments or 'get_latest_by' in the model's Meta."
891
- )
892
- obj = self._chain()
893
- obj.query.set_limits(high=1)
894
- obj.query.clear_ordering(force=True)
895
- obj.query.add_ordering(*order_by)
896
- return obj.get()
897
-
898
- def earliest(self, *fields):
899
- if self.query.is_sliced:
900
- raise TypeError("Cannot change a query once a slice has been taken.")
901
- return self._earliest(*fields)
902
-
903
- def latest(self, *fields):
904
- """
905
- Return the latest object according to fields (if given) or by the
906
- model's Meta.get_latest_by.
907
- """
908
- if self.query.is_sliced:
909
- raise TypeError("Cannot change a query once a slice has been taken.")
910
- return self.reverse()._earliest(*fields)
911
-
912
876
  def first(self):
913
877
  """Return the first object of a query or None if no match is found."""
914
- if self.ordered:
915
- queryset = self
916
- else:
917
- self._check_ordering_first_last_queryset_aggregation(method="first")
918
- queryset = self.order_by("id")
919
- for obj in queryset[:1]:
878
+ for obj in self[:1]:
920
879
  return obj
921
880
 
922
881
  def last(self):
923
882
  """Return the last object of a query or None if no match is found."""
924
- if self.ordered:
925
- queryset = self.reverse()
926
- else:
927
- self._check_ordering_first_last_queryset_aggregation(method="last")
928
- queryset = self.order_by("-id")
883
+ queryset = self.reverse()
929
884
  for obj in queryset[:1]:
930
885
  return obj
931
886
 
@@ -1763,16 +1718,6 @@ class QuerySet:
1763
1718
  if self.query.combinator or other.query.combinator:
1764
1719
  raise TypeError(f"Cannot use {operator_} operator with combined queryset.")
1765
1720
 
1766
- def _check_ordering_first_last_queryset_aggregation(self, method):
1767
- if isinstance(self.query.group_by, tuple) and not any(
1768
- col.output_field is self.model._meta.get_field("id")
1769
- for col in self.query.group_by
1770
- ):
1771
- raise TypeError(
1772
- f"Cannot use QuerySet.{method}() on an unordered queryset performing "
1773
- f"aggregation. Add an ordering with order_by()."
1774
- )
1775
-
1776
1721
 
1777
1722
  class InstanceCheckMeta(type):
1778
1723
  def __instancecheck__(self, instance):
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.40.0
3
+ Version: 0.41.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
7
- Requires-Python: >=3.11
7
+ Requires-Python: >=3.13
8
8
  Requires-Dist: plain<1.0.0
9
9
  Requires-Dist: sqlparse>=0.3.1
10
10
  Description-Content-Type: text/markdown
@@ -1,4 +1,5 @@
1
- plain/models/CHANGELOG.md,sha256=aNi4zsWMbFRKdV1O5x9Du-fKepQl3jj6x7Ksu3rcIXI,9295
1
+ plain/models/AGENTS.md,sha256=xQQW-z-DehnCUyjiGSBfLqUjoSUdo_W1b0JmwYmWieA,209
2
+ plain/models/CHANGELOG.md,sha256=2A4tByZg03dkzz_A5larJoF2eZx6rDtNPrAto0f2i1I,10788
2
3
  plain/models/README.md,sha256=uibhtLwH-JUGzpW3tCFUrQo19i2RAvet1EkpyItdFxM,7269
3
4
  plain/models/__init__.py,sha256=LJhlJauhTfUySY2hTJ9qBhCbEKMxTDKpeVrjYXZnsCw,2964
4
5
  plain/models/aggregates.py,sha256=P0mhsMl1VZt2CVHMuCHnNI8SxZ9citjDLEgioN6NOpo,7240
@@ -20,10 +21,10 @@ plain/models/forms.py,sha256=FUBgt1P-4JmSQeigdITYqZPYqDI89XZQfPtdr8ffgsc,25816
20
21
  plain/models/indexes.py,sha256=fazIZPJgCX5_Bhwk7MQy3YbWOxpHvaCe1dDLGGldTuY,11540
21
22
  plain/models/lookups.py,sha256=eCsxQXUcOoAa_U_fAAd3edcgXI1wfyFW8hPgUh8TwTo,24776
22
23
  plain/models/manager.py,sha256=zc2W-vTTk3zkDXCds5-TCXgLhVmM4PdQb-qtu-njeLQ,5827
23
- plain/models/options.py,sha256=jiFlj1jQ9FH4uNrNePGEWwfrs8rLvDaHTIw9d1zMgAk,23627
24
- plain/models/otel.py,sha256=OCG6ZXbaQmAwvjjAHwH6ISFXJ651W942m2dFu87Pmi8,5893
24
+ plain/models/options.py,sha256=1EtFAXG3RqwoT8DzRKUmZvfXlhk98MvCf4q4FG6jBN4,23572
25
+ plain/models/otel.py,sha256=BtNx0ZqDmBD9nzWV6FboEJ2j8kxPDmNhSl52VfXF-bY,7168
25
26
  plain/models/preflight.py,sha256=PlS1S2YHEpSKZ57KbTP6TbED98dDDXYSBUk6xMIpgsI,8136
26
- plain/models/query.py,sha256=T_1H533CuxaenHEWKovRq-4QF2rTqGtq_4vw7BnfWgY,92653
27
+ plain/models/query.py,sha256=i9wDYzi3hjlLSjUih3yykwERLZT23m1gZ56jqmZth7Y,90520
27
28
  plain/models/query_utils.py,sha256=Ny7PZJ5GduDrdzAT2ALtcKge780QP3w-ARTBqHNOu6U,14178
28
29
  plain/models/registry.py,sha256=5yxVgT_W8GlyL2bsGT2HvMQB5sKolXucP2qrhr7Wlnk,8126
29
30
  plain/models/transaction.py,sha256=KqkRDT6aqMgbPA_ch7qO8a9NyDvwY_2FaxM7FkBkcgY,9357
@@ -102,7 +103,7 @@ plain/models/migrations/writer.py,sha256=N8Rnjv5ccsA_CTcS7zZyppzyHFOUQVJy0l6RZYj
102
103
  plain/models/migrations/operations/__init__.py,sha256=C8VTJbzvFgt7AXTPvtuY8HF1FC8rqNvsXMOCW26UV9E,802
103
104
  plain/models/migrations/operations/base.py,sha256=JsKGjM6ouvEbFHzV14km7YjkpOUC4PoUR1M2yGZ82bE,4323
104
105
  plain/models/migrations/operations/fields.py,sha256=ARL945rbztAnMsbd0lvQRsQJEmxYA3gDof0-4aOTeC4,11255
105
- plain/models/migrations/operations/models.py,sha256=bdsEhCCi6UJVxKaNA4wJUGrU9ZmeqZZItJUBZ-2a4TM,26855
106
+ plain/models/migrations/operations/models.py,sha256=kZc9zYK3Q3NyLV98oj-RYRyocIbFdt4QNyryOrQJAMM,26830
106
107
  plain/models/migrations/operations/special.py,sha256=SiL_7u3rSj35uhc8JPmFHtItt_k_EgbLhY-TEXfmkaI,5162
107
108
  plain/models/sql/__init__.py,sha256=FoRCcab-kh_XY8C4eldgLy9-zuk-M63Nyi9cFsYjclU,225
108
109
  plain/models/sql/compiler.py,sha256=UzqJljjKfjidz8TW0tr8CucLF2Y0xmdsW9Rpnm_1Sc4,84701
@@ -114,8 +115,8 @@ plain/models/sql/where.py,sha256=ezE9Clt2BmKo-I7ARsgqZ_aVA-1UdayCwr6ULSWZL6c,126
114
115
  plain/models/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
116
  plain/models/test/pytest.py,sha256=KD5-mxonBxOYIhUh9Ql5uJOIiC9R4t-LYfb6sjA0UdE,3486
116
117
  plain/models/test/utils.py,sha256=S3d6zf3OFWDxB_kBJr0tDvwn51bjwDVWKPumv37N-p8,467
117
- plain_models-0.40.0.dist-info/METADATA,sha256=jIaS3eoDJumh9HhSOzA-bXfTYB75h5MketBZH37kV0Q,7581
118
- plain_models-0.40.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
119
- plain_models-0.40.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
120
- plain_models-0.40.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
121
- plain_models-0.40.0.dist-info/RECORD,,
118
+ plain_models-0.41.0.dist-info/METADATA,sha256=9Qxx7j7nc5sZ-bYTz1IEcV4Bql9c_mHj1ar9qkg9O6g,7581
119
+ plain_models-0.41.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
+ plain_models-0.41.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
+ plain_models-0.41.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
+ plain_models-0.41.0.dist-info/RECORD,,