Skip to content

Commit cb897fd

Browse files
committed
Continue refactoring and correcting tests
1 parent 9a9f4ef commit cb897fd

20 files changed

+1507
-1141
lines changed

docs/reference.md

+37-9
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,47 @@ Package settings.
2424
2525
.. data:: POSTGRES_RANGE_FIELDS
2626
27-
Dictionary of field name and Python type that should be used to represent the range field. Default is:
27+
Dictionary of model range fields and the associated Python types that should be used to represent a boundary value, a delta value, and a range. Default is:
2828
2929
.. code-block:: python
3030
3131
{
32-
IntegerRangeField.__name__: int,
33-
BigIntegerRangeField.__name__: int,
34-
DecimalRangeField.__name__: Decimal,
35-
DateRangeField.__name__: date,
36-
DateTimeRangeField.__name__: datetime,
32+
IntegerRangeField: {
33+
"value_type": int,
34+
"delta_type": int,
35+
"range_type": NumericRange,
36+
},
37+
BigIntegerRangeField: {
38+
"value_type": int,
39+
"delta_type": int,
40+
"range_type": NumericRange,
41+
},
42+
DecimalRangeField: {
43+
"value_type": Decimal,
44+
"delta_type": Decimal,
45+
"range_type": NumericRange,
46+
},
47+
DateRangeField: {
48+
"value_type": date,
49+
"delta_type": timezone.timedelta,
50+
"range_type": DateRange,
51+
},
52+
DateTimeRangeField: {
53+
"value_type": datetime,
54+
"delta_type": timezone.timedelta,
55+
"range_type": DateTimeTZRange,
56+
}
3757
}
3858
39-
This is used to convert the range field to a Python type when using the :meth:`django_segments.models.base.BaseSpanMetaclass.get_range_field` method.
59+
This is used to convert the range field to a Python type, and for validation when creating a new Span or Segment.
60+
61+
.. data:: DEFAULT_RELATED_NAME
62+
63+
Default related name for the Span and Segment models. Default is ``%(app_label)s_%(class)s_related``.
64+
65+
.. data:: DEFAULT_RELATED_QUERY_NAME
66+
67+
Default related query name for the Span and Segment models. Default is ``%(app_label)s_%(class)ss``.
4068
4169
Global Span Configuration Options
4270
---------------------------------
@@ -84,11 +112,11 @@ more of the corresponding setting names in lowercase to the segment model. Examp
84112
85113
.. data:: PREVIOUS_FIELD_ON_DELETE
86114
87-
The behavior to use when deleting a segment or span that has a previous segment or span. Default is :attr:`django.db.models.CASCADE`.
115+
The behavior to use when deleting a segment that has a previous segment. Default is :attr:`django.db.models.CASCADE`.
88116
89117
-- data:: SPAN_ON_DELETE
90118
91-
The behavior to use when deleting a span. Default is :attr:`django.db.models.CASCADE`.
119+
The behavior to use for segment instances with foreign key to a deleted span. Default is :attr:`django.db.models.CASCADE`.
92120
93121
94122

src/django_segments/app_settings.py

+21-16
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
same attribute on the concrete Span model. The default value is `True`. If `True`, the `deleted_at` field will
3232
be added to the model and used for soft deletion.
3333
"""
34-
3534
import logging
3635
from datetime import date, datetime
3736
from decimal import Decimal
@@ -51,6 +50,7 @@
5150
NumericRange,
5251
)
5352
from django.db.models.base import ModelBase
53+
from django.utils import timezone
5454

5555

5656
logger = logging.getLogger(__name__)
@@ -65,25 +65,30 @@
6565
settings,
6666
"POSTGRES_RANGE_FIELDS",
6767
{
68-
IntegerRangeField.__name__: {
69-
"type": int,
70-
"range": NumericRange,
68+
IntegerRangeField: {
69+
"value_type": int,
70+
"delta_type": int,
71+
"range_type": NumericRange,
7172
},
72-
BigIntegerRangeField.__name__: {
73-
"type": int,
74-
"range": NumericRange,
73+
BigIntegerRangeField: {
74+
"value_type": int,
75+
"delta_type": int,
76+
"range_type": NumericRange,
7577
},
76-
DecimalRangeField.__name__: {
77-
"type": Decimal,
78-
"range": NumericRange,
78+
DecimalRangeField: {
79+
"value_type": Decimal,
80+
"delta_type": Decimal,
81+
"range_type": NumericRange,
7982
},
80-
DateRangeField.__name__: {
81-
"type": date,
82-
"range": DateRange,
83+
DateRangeField: {
84+
"value_type": date,
85+
"delta_type": timezone.timedelta,
86+
"range_type": DateRange,
8387
},
84-
DateTimeRangeField.__name__: {
85-
"type": datetime,
86-
"range": DateTimeTZRange,
88+
DateTimeRangeField: {
89+
"value_type": datetime,
90+
"delta_type": timezone.timedelta,
91+
"range_type": DateTimeTZRange,
8792
},
8893
},
8994
)

src/django_segments/context_managers.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
"""Context managers for sending signals before and after creating, updating, and deleting spans and segments."""
2+
3+
from __future__ import annotations
4+
15
import logging
6+
import typing
27

3-
from django_segments.models.base import (
4-
BaseSegmentMetaclass,
5-
SegmentConfigurationHelper,
6-
boundary_helper_factory,
7-
)
8+
from django.db.backends.postgresql.psycopg_any import Range
9+
10+
from django_segments.models.base import BaseSegmentMetaclass, SegmentConfigurationHelper
811

912
from .signals import (
1013
segment_create_failed,
@@ -38,6 +41,9 @@
3841

3942
logger = logging.getLogger(__name__)
4043

44+
if typing.TYPE_CHECKING:
45+
from django_segments.models import AbstractSpan
46+
4147

4248
class SpanCreateSignalContext:
4349
"""Context manager for sending signals before and after creating a span.
@@ -51,10 +57,9 @@ class SpanCreateSignalContext:
5157
context.kwargs["span"] = span
5258
"""
5359

54-
def __init__(self, span_model, span_range, *args, **kwargs):
60+
def __init__(self, *, span_model, span_range, **kwargs):
5561
self.span_model = span_model
5662
self.span_range = span_range
57-
self.args = args
5863
self.kwargs = kwargs
5964

6065
def __enter__(self):
@@ -174,15 +179,14 @@ class SegmentCreateSignalContext:
174179
175180
.. code-block:: python
176181
177-
with SegmentCreateSignalContext(span, segment_range) as context:
182+
with SegmentCreateSignalContext(span=span, segment_range=segment_range) as context:
178183
segment = Segment.objects.create(span=span, segment_range=segment_range)
179184
context.kwargs["segment"] = segment
180185
"""
181186

182-
def __init__(self, span, segment_range, *args, **kwargs):
187+
def __init__(self, *, span: AbstractSpan, segment_range: Range, **kwargs):
183188
self.span = span
184189
self.segment_range = segment_range
185-
self.args = args
186190
self.kwargs = kwargs
187191

188192
def __enter__(self):

src/django_segments/helpers/base.py

+87-31
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from enum import Enum, auto
88
from typing import TYPE_CHECKING, Type, Union
99

10+
from django.contrib.postgres.fields import (
11+
BigIntegerRangeField,
12+
DateRangeField,
13+
DateTimeRangeField,
14+
DecimalRangeField,
15+
IntegerRangeField,
16+
RangeOperators,
17+
)
1018
from django.core.exceptions import FieldDoesNotExist
1119
from django.db.backends.postgresql.psycopg_any import (
1220
DateRange,
@@ -26,81 +34,129 @@
2634
from django_segments.models import AbstractSegment, AbstractSpan
2735

2836

37+
def get_allowed_postgres_range_field_type_names() -> list[str]:
38+
"""Get the names of all allowed PostgreSQL range field types."""
39+
return [type.__name__ for type in POSTGRES_RANGE_FIELDS.keys()]
40+
41+
42+
def get_allowed_postgres_range_field_types() -> list[str]:
43+
"""Get the allowed PostgreSQL range field types."""
44+
return list(POSTGRES_RANGE_FIELDS.keys())
45+
46+
2947
class BoundaryType(Enum): # pylint: disable=C0115
3048
LOWER = auto()
3149
UPPER = auto()
3250

3351

3452
class BaseHelper: # pylint: disable=R0903
35-
"""Base class for all segment and span helpers."""
53+
"""Base class for all segment and span helpers.
54+
55+
Provides common methods and attributes for all segment and span helpers. It should not be instantiated directly.
56+
"""
3657

3758
def __init__(self, obj: Union[AbstractSpan, AbstractSegment]):
3859
self.obj = obj
39-
self.range_field_type = None
40-
self.field_value_type = None
41-
self._initialize_range_field()
60+
self.range_field_type = obj.range_field_type
61+
self.validate_range_field_type()
62+
63+
self.value_type = self._get_value_type(self.range_field_type)
64+
self.delta_value_type = self._get_delta_value_type(self.range_field_type)
65+
self.range_type = self._get_range_type(self.range_field_type)
4266

43-
def _initialize_range_field(self) -> None:
67+
self.range_field_type_name = ""
68+
self.field_value_type_name = ""
69+
self._initialize_type_names()
70+
71+
def _initialize_type_names(self) -> None:
4472
"""Initialize the range field type and value type."""
4573
for field_name in ["current_range", "segment_range"]:
4674
if hasattr(self.obj, field_name):
4775
range_value = getattr(self.obj, field_name)
4876
range_field = self._get_range_field(field_name)
4977
if range_field:
50-
self.range_field_type = range_field.get_internal_type()
51-
self.field_value_type = type(range_value).__name__
78+
self.range_field_type_name = range_field.get_internal_type()
79+
self.field_value_type_name = type(range_value).__name__
5280
return
5381
raise ValueError("Object must have either a `segment_range` or `current_range` field.")
5482

55-
def _get_range_field(self, field_name: str) -> Type:
83+
def _get_range_field(
84+
self, field_name: str
85+
) -> Union[IntegerRangeField, BigIntegerRangeField, DecimalRangeField, DateRangeField, DateTimeRangeField]:
5686
"""Get the range field from the model."""
5787
try:
5888
return self.obj._meta.get_field(field_name) # pylint: disable=W0212
5989
except FieldDoesNotExist as e:
6090
logger.error("FieldDoesNotExist error: %s", e)
6191
return None
6292

93+
def validate_range_field_type(self) -> None:
94+
"""Validate that the range field type is allowed."""
95+
if self.range_field_type not in POSTGRES_RANGE_FIELDS:
96+
raise ValueError(
97+
f"Unsupported field type for `segment_range` field: "
98+
f"{self.range_field_type=} not in {POSTGRES_RANGE_FIELDS=}"
99+
)
100+
63101
def validate_value_type(self, value: Union[int, Decimal, date, datetime]) -> None:
64102
"""Validate the type of the provided value against the model's range_field_type."""
65103
if value is None:
66104
raise ValueError("Value cannot be None")
67105

68-
if self.range_field_type not in POSTGRES_RANGE_FIELDS:
106+
expected_value_type = self._get_value_type(self.range_field_type)
107+
if not isinstance(value, expected_value_type):
69108
raise ValueError(
70-
f"Unsupported field type for `segment_range` field: "
71-
f"{self.range_field_type=} not in {POSTGRES_RANGE_FIELDS.keys()=}"
109+
f"BaseHelper.validate_value_type(): Value must be of type {expected_value_type.__name__}, "
110+
f"not {type(value).__name__}. Provided value: {value}."
72111
)
73112

74-
expected_type = self._get_expected_type(self.range_field_type)
75-
if not isinstance(value, expected_type):
113+
def validate_delta_value_type(self, delta_value: Union[int, Decimal, timezone.timedelta]) -> None:
114+
"""Validate the type of the provided delta value against the model's range_field_type."""
115+
if delta_value is None:
116+
raise ValueError("Delta value cannot be None")
117+
118+
expected_delta_value_type = self._get_delta_value_type(self.range_field_type)
119+
if not isinstance(delta_value, expected_delta_value_type):
76120
raise ValueError(
77-
f"BaseHelper.validate_value_type(): Value must be of type {expected_type.__name__}, "
78-
f"not {type(value).__name__}. Provided value: {value}."
121+
"BaseHelper.validate_delta_value_type(): Delta value must be of type "
122+
f"{expected_delta_value_type.__name__}, "
123+
f"not {type(delta_value).__name__}. Provided delta value: {delta_value}."
79124
)
80125

81126
@staticmethod
82-
def _get_expected_type(range_field_type: str) -> Type:
127+
def _get_value_type(
128+
range_field_type: get_allowed_postgres_range_field_types(),
129+
) -> Union[type[int], type[Decimal], type[date], type[datetime]]:
83130
"""Get the expected type for a given range field type."""
84131
for key, val in POSTGRES_RANGE_FIELDS.items():
85-
if key in range_field_type:
86-
return val.get("type")
87-
raise ValueError(f"No expected type found for range field type: {range_field_type}")
132+
if key is range_field_type:
133+
return val.get("value_type")
134+
raise ValueError(f"No value type found for range field type: {range_field_type}")
135+
136+
@staticmethod
137+
def _get_delta_value_type(
138+
range_field_type: get_allowed_postgres_range_field_types(),
139+
) -> Union[type[int], type[Decimal], type[timezone.timedelta]]:
140+
"""Get the expected type for a given range field type."""
141+
for key, val in POSTGRES_RANGE_FIELDS.items():
142+
if key is range_field_type:
143+
return val.get("delta_type")
144+
raise ValueError(f"No delta type found for range field type: {range_field_type}")
145+
146+
@staticmethod
147+
def _get_range_type(range_field_type: get_allowed_postgres_range_field_types()) -> Type[Range]:
148+
"""Get the range type from the range field type."""
149+
for key, val in POSTGRES_RANGE_FIELDS.items():
150+
if key is range_field_type:
151+
print(f"_get_range_type {val=} {val.get('range_type')=}")
152+
return val.get("range_type")
153+
raise ValueError(f"No range type found for range field type: {range_field_type}")
88154

89155
def set_boundary(
90-
self, range_field: Range, new_boundary: Union[int, Decimal, datetime, date], boundary_type: BoundaryType
156+
self, *, range_field: Range, new_boundary: Union[int, Decimal, datetime, date], boundary_type: BoundaryType
91157
) -> Range:
92-
"""Set the boundary of the range field."""
158+
"""Set the boundary of the model range field."""
93159
return range_field.__class__(
94160
lower=new_boundary if boundary_type == BoundaryType.LOWER else range_field.lower,
95161
upper=new_boundary if boundary_type == BoundaryType.UPPER else range_field.upper,
96162
)
97-
98-
def validate_range(
99-
self,
100-
range_value: Union[Range, DateRange, DateTimeTZRange, NumericRange],
101-
lower_bound: Union[int, Decimal, datetime, date],
102-
upper_bound: Union[int, Decimal, datetime, date],
103-
) -> None:
104-
"""Validate that the range is within the specified bounds."""
105-
if range_value.lower < lower_bound or range_value.upper > upper_bound:
106-
raise ValueError("Range must be within the specified bounds.")

0 commit comments

Comments
 (0)