plain.models 0.40.1__py3-none-any.whl → 0.41.1__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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.41.1](https://github.com/dropseed/plain/releases/plain-models@0.41.1) (2025-09-09)
4
+
5
+ ### What's changed
6
+
7
+ - Improved stack trace filtering in OpenTelemetry spans to exclude internal plain/models frames, making debugging traces cleaner and more focused on user code ([5771dd5](https://github.com/dropseed/plain/commit/5771dd5))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
13
+ ## [0.41.0](https://github.com/dropseed/plain/releases/plain-models@0.41.0) (2025-09-09)
14
+
15
+ ### What's changed
16
+
17
+ - Python 3.13 is now the minimum required version ([d86e307](https://github.com/dropseed/plain/commit/d86e307))
18
+ - 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))
19
+ - 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))
20
+ - 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))
21
+
22
+ ### Upgrade instructions
23
+
24
+ - Replace usage of `earliest()`, `latest()`, and model `Meta` `get_latest_by` queryset methods with equivalent `order_by().first()` or `order_by().last()` calls
25
+ - 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
26
+
3
27
  ## [0.40.1](https://github.com/dropseed/plain/releases/plain-models@0.40.1) (2025-09-03)
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,55 @@ 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
+ # Filter out internal frames from the stack trace
222
+ filtered_stack = []
223
+ for frame in stack:
224
+ filepath = frame.filename
225
+ if not filepath:
226
+ continue
227
+ if "/plain/models/" in filepath:
228
+ continue
229
+ if filepath.endswith("contextlib.py"):
230
+ continue
231
+ filtered_stack.append(frame)
232
+
233
+ attrs[CODE_STACKTRACE] = "".join(traceback.format_list(filtered_stack))
234
+
235
+ return attrs
236
+
237
+ 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.1
3
+ Version: 0.41.1
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,5 +1,5 @@
1
1
  plain/models/AGENTS.md,sha256=xQQW-z-DehnCUyjiGSBfLqUjoSUdo_W1b0JmwYmWieA,209
2
- plain/models/CHANGELOG.md,sha256=f47T5-zyFFFqERNBUuRgdBLoz-6t9VF53TuuQ4Hrjrw,9575
2
+ plain/models/CHANGELOG.md,sha256=NMKkqzD7d6x3kmJzOL0-3qHGjBlFur1pX39ZR7w2i4Y,11169
3
3
  plain/models/README.md,sha256=uibhtLwH-JUGzpW3tCFUrQo19i2RAvet1EkpyItdFxM,7269
4
4
  plain/models/__init__.py,sha256=LJhlJauhTfUySY2hTJ9qBhCbEKMxTDKpeVrjYXZnsCw,2964
5
5
  plain/models/aggregates.py,sha256=P0mhsMl1VZt2CVHMuCHnNI8SxZ9citjDLEgioN6NOpo,7240
@@ -21,10 +21,10 @@ plain/models/forms.py,sha256=FUBgt1P-4JmSQeigdITYqZPYqDI89XZQfPtdr8ffgsc,25816
21
21
  plain/models/indexes.py,sha256=fazIZPJgCX5_Bhwk7MQy3YbWOxpHvaCe1dDLGGldTuY,11540
22
22
  plain/models/lookups.py,sha256=eCsxQXUcOoAa_U_fAAd3edcgXI1wfyFW8hPgUh8TwTo,24776
23
23
  plain/models/manager.py,sha256=zc2W-vTTk3zkDXCds5-TCXgLhVmM4PdQb-qtu-njeLQ,5827
24
- plain/models/options.py,sha256=jiFlj1jQ9FH4uNrNePGEWwfrs8rLvDaHTIw9d1zMgAk,23627
25
- plain/models/otel.py,sha256=OCG6ZXbaQmAwvjjAHwH6ISFXJ651W942m2dFu87Pmi8,5893
24
+ plain/models/options.py,sha256=1EtFAXG3RqwoT8DzRKUmZvfXlhk98MvCf4q4FG6jBN4,23572
25
+ plain/models/otel.py,sha256=36QSJS6UXv1YPJTqeSmEvdMVHRkXa_zgqqItJaXc59g,7619
26
26
  plain/models/preflight.py,sha256=PlS1S2YHEpSKZ57KbTP6TbED98dDDXYSBUk6xMIpgsI,8136
27
- plain/models/query.py,sha256=T_1H533CuxaenHEWKovRq-4QF2rTqGtq_4vw7BnfWgY,92653
27
+ plain/models/query.py,sha256=i9wDYzi3hjlLSjUih3yykwERLZT23m1gZ56jqmZth7Y,90520
28
28
  plain/models/query_utils.py,sha256=Ny7PZJ5GduDrdzAT2ALtcKge780QP3w-ARTBqHNOu6U,14178
29
29
  plain/models/registry.py,sha256=5yxVgT_W8GlyL2bsGT2HvMQB5sKolXucP2qrhr7Wlnk,8126
30
30
  plain/models/transaction.py,sha256=KqkRDT6aqMgbPA_ch7qO8a9NyDvwY_2FaxM7FkBkcgY,9357
@@ -103,7 +103,7 @@ plain/models/migrations/writer.py,sha256=N8Rnjv5ccsA_CTcS7zZyppzyHFOUQVJy0l6RZYj
103
103
  plain/models/migrations/operations/__init__.py,sha256=C8VTJbzvFgt7AXTPvtuY8HF1FC8rqNvsXMOCW26UV9E,802
104
104
  plain/models/migrations/operations/base.py,sha256=JsKGjM6ouvEbFHzV14km7YjkpOUC4PoUR1M2yGZ82bE,4323
105
105
  plain/models/migrations/operations/fields.py,sha256=ARL945rbztAnMsbd0lvQRsQJEmxYA3gDof0-4aOTeC4,11255
106
- plain/models/migrations/operations/models.py,sha256=bdsEhCCi6UJVxKaNA4wJUGrU9ZmeqZZItJUBZ-2a4TM,26855
106
+ plain/models/migrations/operations/models.py,sha256=kZc9zYK3Q3NyLV98oj-RYRyocIbFdt4QNyryOrQJAMM,26830
107
107
  plain/models/migrations/operations/special.py,sha256=SiL_7u3rSj35uhc8JPmFHtItt_k_EgbLhY-TEXfmkaI,5162
108
108
  plain/models/sql/__init__.py,sha256=FoRCcab-kh_XY8C4eldgLy9-zuk-M63Nyi9cFsYjclU,225
109
109
  plain/models/sql/compiler.py,sha256=UzqJljjKfjidz8TW0tr8CucLF2Y0xmdsW9Rpnm_1Sc4,84701
@@ -115,8 +115,8 @@ plain/models/sql/where.py,sha256=ezE9Clt2BmKo-I7ARsgqZ_aVA-1UdayCwr6ULSWZL6c,126
115
115
  plain/models/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  plain/models/test/pytest.py,sha256=KD5-mxonBxOYIhUh9Ql5uJOIiC9R4t-LYfb6sjA0UdE,3486
117
117
  plain/models/test/utils.py,sha256=S3d6zf3OFWDxB_kBJr0tDvwn51bjwDVWKPumv37N-p8,467
118
- plain_models-0.40.1.dist-info/METADATA,sha256=4tylNlEN55nQYCWTRQisZW-F1SKJnf9kKqUojkIXiMQ,7581
119
- plain_models-0.40.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
- plain_models-0.40.1.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
- plain_models-0.40.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
- plain_models-0.40.1.dist-info/RECORD,,
118
+ plain_models-0.41.1.dist-info/METADATA,sha256=enNWwlsqHqqrPDy5uzkFcxfT4bcRO4lYKMjN7W7M6hI,7581
119
+ plain_models-0.41.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
+ plain_models-0.41.1.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
+ plain_models-0.41.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
+ plain_models-0.41.1.dist-info/RECORD,,