Skip to content

Commit

Permalink
Continue refactoring and correcting tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jacklinke committed Jul 5, 2024
1 parent 9a9f4ef commit cb897fd
Show file tree
Hide file tree
Showing 20 changed files with 1,507 additions and 1,141 deletions.
46 changes: 37 additions & 9 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,47 @@ Package settings.
.. data:: POSTGRES_RANGE_FIELDS
Dictionary of field name and Python type that should be used to represent the range field. Default is:
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:
.. code-block:: python
{
IntegerRangeField.__name__: int,
BigIntegerRangeField.__name__: int,
DecimalRangeField.__name__: Decimal,
DateRangeField.__name__: date,
DateTimeRangeField.__name__: datetime,
IntegerRangeField: {
"value_type": int,
"delta_type": int,
"range_type": NumericRange,
},
BigIntegerRangeField: {
"value_type": int,
"delta_type": int,
"range_type": NumericRange,
},
DecimalRangeField: {
"value_type": Decimal,
"delta_type": Decimal,
"range_type": NumericRange,
},
DateRangeField: {
"value_type": date,
"delta_type": timezone.timedelta,
"range_type": DateRange,
},
DateTimeRangeField: {
"value_type": datetime,
"delta_type": timezone.timedelta,
"range_type": DateTimeTZRange,
}
}
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.
This is used to convert the range field to a Python type, and for validation when creating a new Span or Segment.
.. data:: DEFAULT_RELATED_NAME
Default related name for the Span and Segment models. Default is ``%(app_label)s_%(class)s_related``.
.. data:: DEFAULT_RELATED_QUERY_NAME
Default related query name for the Span and Segment models. Default is ``%(app_label)s_%(class)ss``.
Global Span Configuration Options
---------------------------------
Expand Down Expand Up @@ -84,11 +112,11 @@ more of the corresponding setting names in lowercase to the segment model. Examp
.. data:: PREVIOUS_FIELD_ON_DELETE
The behavior to use when deleting a segment or span that has a previous segment or span. Default is :attr:`django.db.models.CASCADE`.
The behavior to use when deleting a segment that has a previous segment. Default is :attr:`django.db.models.CASCADE`.
-- data:: SPAN_ON_DELETE
The behavior to use when deleting a span. Default is :attr:`django.db.models.CASCADE`.
The behavior to use for segment instances with foreign key to a deleted span. Default is :attr:`django.db.models.CASCADE`.
Expand Down
37 changes: 21 additions & 16 deletions src/django_segments/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
same attribute on the concrete Span model. The default value is `True`. If `True`, the `deleted_at` field will
be added to the model and used for soft deletion.
"""

import logging
from datetime import date, datetime
from decimal import Decimal
Expand All @@ -51,6 +50,7 @@
NumericRange,
)
from django.db.models.base import ModelBase
from django.utils import timezone


logger = logging.getLogger(__name__)
Expand All @@ -65,25 +65,30 @@
settings,
"POSTGRES_RANGE_FIELDS",
{
IntegerRangeField.__name__: {
"type": int,
"range": NumericRange,
IntegerRangeField: {
"value_type": int,
"delta_type": int,
"range_type": NumericRange,
},
BigIntegerRangeField.__name__: {
"type": int,
"range": NumericRange,
BigIntegerRangeField: {
"value_type": int,
"delta_type": int,
"range_type": NumericRange,
},
DecimalRangeField.__name__: {
"type": Decimal,
"range": NumericRange,
DecimalRangeField: {
"value_type": Decimal,
"delta_type": Decimal,
"range_type": NumericRange,
},
DateRangeField.__name__: {
"type": date,
"range": DateRange,
DateRangeField: {
"value_type": date,
"delta_type": timezone.timedelta,
"range_type": DateRange,
},
DateTimeRangeField.__name__: {
"type": datetime,
"range": DateTimeTZRange,
DateTimeRangeField: {
"value_type": datetime,
"delta_type": timezone.timedelta,
"range_type": DateTimeTZRange,
},
},
)
Expand Down
24 changes: 14 additions & 10 deletions src/django_segments/context_managers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Context managers for sending signals before and after creating, updating, and deleting spans and segments."""

from __future__ import annotations

import logging
import typing

from django_segments.models.base import (
BaseSegmentMetaclass,
SegmentConfigurationHelper,
boundary_helper_factory,
)
from django.db.backends.postgresql.psycopg_any import Range

from django_segments.models.base import BaseSegmentMetaclass, SegmentConfigurationHelper

from .signals import (
segment_create_failed,
Expand Down Expand Up @@ -38,6 +41,9 @@

logger = logging.getLogger(__name__)

if typing.TYPE_CHECKING:
from django_segments.models import AbstractSpan


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

def __init__(self, span_model, span_range, *args, **kwargs):
def __init__(self, *, span_model, span_range, **kwargs):
self.span_model = span_model
self.span_range = span_range
self.args = args
self.kwargs = kwargs

def __enter__(self):
Expand Down Expand Up @@ -174,15 +179,14 @@ class SegmentCreateSignalContext:
.. code-block:: python
with SegmentCreateSignalContext(span, segment_range) as context:
with SegmentCreateSignalContext(span=span, segment_range=segment_range) as context:
segment = Segment.objects.create(span=span, segment_range=segment_range)
context.kwargs["segment"] = segment
"""

def __init__(self, span, segment_range, *args, **kwargs):
def __init__(self, *, span: AbstractSpan, segment_range: Range, **kwargs):
self.span = span
self.segment_range = segment_range
self.args = args
self.kwargs = kwargs

def __enter__(self):
Expand Down
118 changes: 87 additions & 31 deletions src/django_segments/helpers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
from enum import Enum, auto
from typing import TYPE_CHECKING, Type, Union

from django.contrib.postgres.fields import (
BigIntegerRangeField,
DateRangeField,
DateTimeRangeField,
DecimalRangeField,
IntegerRangeField,
RangeOperators,
)
from django.core.exceptions import FieldDoesNotExist
from django.db.backends.postgresql.psycopg_any import (
DateRange,
Expand All @@ -26,81 +34,129 @@
from django_segments.models import AbstractSegment, AbstractSpan


def get_allowed_postgres_range_field_type_names() -> list[str]:
"""Get the names of all allowed PostgreSQL range field types."""
return [type.__name__ for type in POSTGRES_RANGE_FIELDS.keys()]


def get_allowed_postgres_range_field_types() -> list[str]:
"""Get the allowed PostgreSQL range field types."""
return list(POSTGRES_RANGE_FIELDS.keys())


class BoundaryType(Enum): # pylint: disable=C0115
LOWER = auto()
UPPER = auto()


class BaseHelper: # pylint: disable=R0903
"""Base class for all segment and span helpers."""
"""Base class for all segment and span helpers.
Provides common methods and attributes for all segment and span helpers. It should not be instantiated directly.
"""

def __init__(self, obj: Union[AbstractSpan, AbstractSegment]):
self.obj = obj
self.range_field_type = None
self.field_value_type = None
self._initialize_range_field()
self.range_field_type = obj.range_field_type
self.validate_range_field_type()

self.value_type = self._get_value_type(self.range_field_type)
self.delta_value_type = self._get_delta_value_type(self.range_field_type)
self.range_type = self._get_range_type(self.range_field_type)

def _initialize_range_field(self) -> None:
self.range_field_type_name = ""
self.field_value_type_name = ""
self._initialize_type_names()

def _initialize_type_names(self) -> None:
"""Initialize the range field type and value type."""
for field_name in ["current_range", "segment_range"]:
if hasattr(self.obj, field_name):
range_value = getattr(self.obj, field_name)
range_field = self._get_range_field(field_name)
if range_field:
self.range_field_type = range_field.get_internal_type()
self.field_value_type = type(range_value).__name__
self.range_field_type_name = range_field.get_internal_type()
self.field_value_type_name = type(range_value).__name__
return
raise ValueError("Object must have either a `segment_range` or `current_range` field.")

def _get_range_field(self, field_name: str) -> Type:
def _get_range_field(
self, field_name: str
) -> Union[IntegerRangeField, BigIntegerRangeField, DecimalRangeField, DateRangeField, DateTimeRangeField]:
"""Get the range field from the model."""
try:
return self.obj._meta.get_field(field_name) # pylint: disable=W0212
except FieldDoesNotExist as e:
logger.error("FieldDoesNotExist error: %s", e)
return None

def validate_range_field_type(self) -> None:
"""Validate that the range field type is allowed."""
if self.range_field_type not in POSTGRES_RANGE_FIELDS:
raise ValueError(
f"Unsupported field type for `segment_range` field: "
f"{self.range_field_type=} not in {POSTGRES_RANGE_FIELDS=}"
)

def validate_value_type(self, value: Union[int, Decimal, date, datetime]) -> None:
"""Validate the type of the provided value against the model's range_field_type."""
if value is None:
raise ValueError("Value cannot be None")

if self.range_field_type not in POSTGRES_RANGE_FIELDS:
expected_value_type = self._get_value_type(self.range_field_type)
if not isinstance(value, expected_value_type):
raise ValueError(
f"Unsupported field type for `segment_range` field: "
f"{self.range_field_type=} not in {POSTGRES_RANGE_FIELDS.keys()=}"
f"BaseHelper.validate_value_type(): Value must be of type {expected_value_type.__name__}, "
f"not {type(value).__name__}. Provided value: {value}."
)

expected_type = self._get_expected_type(self.range_field_type)
if not isinstance(value, expected_type):
def validate_delta_value_type(self, delta_value: Union[int, Decimal, timezone.timedelta]) -> None:
"""Validate the type of the provided delta value against the model's range_field_type."""
if delta_value is None:
raise ValueError("Delta value cannot be None")

expected_delta_value_type = self._get_delta_value_type(self.range_field_type)
if not isinstance(delta_value, expected_delta_value_type):
raise ValueError(
f"BaseHelper.validate_value_type(): Value must be of type {expected_type.__name__}, "
f"not {type(value).__name__}. Provided value: {value}."
"BaseHelper.validate_delta_value_type(): Delta value must be of type "
f"{expected_delta_value_type.__name__}, "
f"not {type(delta_value).__name__}. Provided delta value: {delta_value}."
)

@staticmethod
def _get_expected_type(range_field_type: str) -> Type:
def _get_value_type(
range_field_type: get_allowed_postgres_range_field_types(),
) -> Union[type[int], type[Decimal], type[date], type[datetime]]:
"""Get the expected type for a given range field type."""
for key, val in POSTGRES_RANGE_FIELDS.items():
if key in range_field_type:
return val.get("type")
raise ValueError(f"No expected type found for range field type: {range_field_type}")
if key is range_field_type:
return val.get("value_type")
raise ValueError(f"No value type found for range field type: {range_field_type}")

@staticmethod
def _get_delta_value_type(
range_field_type: get_allowed_postgres_range_field_types(),
) -> Union[type[int], type[Decimal], type[timezone.timedelta]]:
"""Get the expected type for a given range field type."""
for key, val in POSTGRES_RANGE_FIELDS.items():
if key is range_field_type:
return val.get("delta_type")
raise ValueError(f"No delta type found for range field type: {range_field_type}")

@staticmethod
def _get_range_type(range_field_type: get_allowed_postgres_range_field_types()) -> Type[Range]:
"""Get the range type from the range field type."""
for key, val in POSTGRES_RANGE_FIELDS.items():
if key is range_field_type:
print(f"_get_range_type {val=} {val.get('range_type')=}")
return val.get("range_type")
raise ValueError(f"No range type found for range field type: {range_field_type}")

def set_boundary(
self, range_field: Range, new_boundary: Union[int, Decimal, datetime, date], boundary_type: BoundaryType
self, *, range_field: Range, new_boundary: Union[int, Decimal, datetime, date], boundary_type: BoundaryType
) -> Range:
"""Set the boundary of the range field."""
"""Set the boundary of the model range field."""
return range_field.__class__(
lower=new_boundary if boundary_type == BoundaryType.LOWER else range_field.lower,
upper=new_boundary if boundary_type == BoundaryType.UPPER else range_field.upper,
)

def validate_range(
self,
range_value: Union[Range, DateRange, DateTimeTZRange, NumericRange],
lower_bound: Union[int, Decimal, datetime, date],
upper_bound: Union[int, Decimal, datetime, date],
) -> None:
"""Validate that the range is within the specified bounds."""
if range_value.lower < lower_bound or range_value.upper > upper_bound:
raise ValueError("Range must be within the specified bounds.")
Loading

0 comments on commit cb897fd

Please sign in to comment.