Skip to content

Commit 96955a2

Browse files
committed
wipwip
1 parent 617096f commit 96955a2

5 files changed

Lines changed: 116 additions & 38 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@ pc up testbox
105105

106106
example devloop -- build with current code, lint and run tests with debugger on error, stop on failure
107107
```
108-
pc run --build --rm --no-deps testbox poetry run python -m elasticsearch_metrics.tests --devloop
108+
pc run --build --rm --no-deps testbox poetry run python -m elasticsearch_metrics.tests --failfast --pdb
109109
```

elasticsearch_metrics/imps/elastic8.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@
2727
from elasticsearch8.exceptions import NotFoundError
2828
from elasticsearch8 import Elasticsearch as Elastic8Client
2929
from elasticsearch8 import dsl as esdsl
30-
from elasticsearch8._sync.document import IndexMeta
30+
from elasticsearch8.dsl._sync.document import IndexMeta
3131

3232
from elasticsearch_metrics import signals
3333
from elasticsearch_metrics import exceptions
3434
from elasticsearch_metrics.registry import djelme_registry
35-
from elasticsearch_metrics.protocols import ProtoDjelmeBackend, ProtoCountedUsage
35+
from elasticsearch_metrics.protocols import (
36+
ProtoDjelmeBackend,
37+
ProtoCountedUsage,
38+
ProtoDjelmeRecord,
39+
)
3640
from elasticsearch_metrics.util import timeseries_naming
3741
from elasticsearch_metrics.util.unique_together import get_unique_id
3842
from elasticsearch_metrics.util.anon_enough import opaque_sessionhour_id
@@ -144,14 +148,16 @@ class Meta:
144148
abstract = True
145149

146150
@classmethod
147-
def record(cls, *, using=None, **kwargs):
151+
def record(
152+
cls, *, using: str | None = None, **kwargs: typing.Any
153+
) -> "typing.Self": # typing.Self added in py 3.11 -- str annotation until 3.10 eol
148154
"""Persist a record in Elasticsearch."""
149155
_instance = cls(**kwargs)
150156
_instance.save(using=using)
151157
return _instance
152158

153159
@classmethod
154-
def check_djelme_setup(cls, using: str | Elastic8Client | None = None) -> bool:
160+
def check_djelme_setup(cls, using: str | None = None) -> bool:
155161
return bool(cls._index.get(using=using))
156162

157163
@classmethod
@@ -225,7 +231,11 @@ class TimeseriesRecord(DjelmeRecordtype):
225231
timestamp: datetime.datetime = esdsl.mapped_field(
226232
default_factory=lambda: django.utils.timezone.now()
227233
)
228-
timeparts: str = esdsl.mapped_field(esdsl.Version())
234+
# the 'version' field type allows range queries on semver-like strings
235+
# that fit perfectly with "timeparts" representation of a UTC datetime
236+
# as a sequence of integers -- use to avoid time zones and date math
237+
# (e.g. '2000' < '2000.5.10' < '2000.5.20.20.20' < '2000.11' < '2001')
238+
timestamp_parts: str = esdsl.mapped_field(esdsl.Version(), default="")
229239

230240
class Meta:
231241
abstract = True
@@ -253,11 +263,16 @@ def search_timeseries_range(
253263
from_when: tuple[int, ...] | datetime.date,
254264
until_when: tuple[int, ...] | datetime.date,
255265
**kwargs: typing.Any,
256-
) -> esdsl.Search:
266+
) -> typing.Any:
257267
_index_pattern = cls.format_timeseries_index_pattern_for_range(
258268
from_when, until_when
259269
)
260-
_timestamp_q = esdsl.query.Range("timestamp", gte=..., lt=...)
270+
_timedepth = cls.get_timedepth()
271+
_timestamp_q = esdsl.query.Range(
272+
"timestamp_parts",
273+
gte=timeseries_naming.semverlike_timeparts(from_when, timedepth=_timedepth),
274+
lt=timeseries_naming.semverlike_timeparts(until_when, timedepth=_timedepth),
275+
)
261276
return cls.search(index=_index_pattern).filter(_timestamp_q)
262277

263278
@classmethod
@@ -425,6 +440,13 @@ def check_djelme_setup(cls, using: str | Elastic8Client | None = None) -> bool:
425440
###
426441
# instance methods
427442

443+
def __init__(self, *args, **kwargs):
444+
super().__init__(*args, **kwargs)
445+
self.timestamp_parts = self._build_timestamp_parts()
446+
447+
def _build_timestamp_parts(self) -> str:
448+
return timeseries_naming.full_semverlike_timeparts(self.timestamp)
449+
428450
def djelme_index_name(self) -> str:
429451
assert self.timestamp is not None
430452
return self.format_timeseries_index_name(self.timestamp)
@@ -567,6 +589,7 @@ def djelme_teardown(self, recordtypes: collections.abc.Iterable[type]) -> None:
567589
# for static type-checking; verify intent
568590
_: type[ProtoCountedUsage] = CountedUsageRecord
569591
__: type[ProtoDjelmeBackend] = DjelmeElastic8Backend
592+
___: type[ProtoDjelmeRecord] = DjelmeRecordtype
570593

571594
###
572595
# names expected by ProtoDjelmeImp

elasticsearch_metrics/protocols.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,13 @@ def djelme_teardown(self, recordtypes: collections.abc.Iterable[type]) -> None:
4444
class ProtoDjelmeRecord(typing.Protocol):
4545
@classmethod
4646
def record(
47-
cls, **kwargs: typing.Any
47+
cls, *, using: str | None = None, **kwargs: typing.Any
4848
) -> "typing.Self": # typing.Self added in py 3.11 -- str annotation until 3.10 eol
4949
...
5050

5151
@classmethod
5252
def check_djelme_setup(cls, using: str | None = None) -> bool: ...
5353

54-
def djelme_index_name(self) -> str: ...
55-
5654
@classmethod
5755
def search_timeseries_range(
5856
cls,
@@ -64,6 +62,8 @@ def search_timeseries_range(
6462
# @classmethod
6563
# def each_timeseries_index_status(cls) -> collections.abc.Iterable[str]: ...
6664

65+
def djelme_index_name(self) -> str: ...
66+
6767

6868
###
6969
# counter?

elasticsearch_metrics/tests/_test_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,5 @@ def setUp(self):
7878
mock.patch("elasticsearch_metrics.imps.elastic6.Document.save"),
7979
)
8080
self.mocked_es8_save = self.enterContext(
81-
mock.patch("elasticsearch_metrics.imps.elastic8.Document.save"),
81+
mock.patch("elasticsearch_metrics.imps.elastic8.esdsl.Document.save"),
8282
)

elasticsearch_metrics/util/timeseries_naming.py

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"parse_index_pattern",
1414
"format_namepart",
1515
"format_template_name",
16+
"timeparts_from_date",
1617
)
1718

1819
import collections
@@ -103,10 +104,8 @@ def _each_timeparts_for_timerange(
103104
if timedepth <= 0:
104105
yield (), True # reached timedepth; wildcard
105106
return
106-
_from_part, *_from_rest = from_timeparts
107-
_until_part, *_until_rest = until_timeparts
108-
assert _from_part <= _until_part
109-
107+
_from_part, *_from_rest = from_timeparts or [0]
108+
_until_part, *_until_rest = until_timeparts or [0]
110109
if include_less_granular:
111110
yield (), False # include less-granular non-wildcard index, if it exists
112111
if _from_part == _until_part:
@@ -118,7 +117,7 @@ def _each_timeparts_for_timerange(
118117
include_less_granular=include_less_granular,
119118
):
120119
yield (_from_part, *_rest_parts), _is_wildcard
121-
elif (_until_part - _from_part) <= max_fanout: # not too far apart
120+
elif 0 < (_until_part - _from_part) <= max_fanout: # not too far apart
122121
for _parallel_part in range(_from_part, _until_part):
123122
yield (_parallel_part,), True # wildcard
124123
if any(_until_rest): # some of the "until" bucket is included
@@ -250,22 +249,13 @@ def format_index_pattern_for_range(
250249
>>> format_index_pattern_for_range('ap', 'rt',
251250
... (200, 5), datetime.date(200, 5, 8),
252251
... timedepth=3)
252+
'ap_rt_200_05_*'
253253
"""
254-
_from_timeparts = (
255-
timeparts_from_date(from_when, timedepth)
256-
if isinstance(from_when, datetime.date)
257-
else from_when
258-
)
259-
_until_timeparts = (
260-
timeparts_from_date(until_when, timedepth)
261-
if isinstance(until_when, datetime.date)
262-
else until_when
263-
)
264254
return format_index_pattern_for_timerange(
265-
prefix,
266-
recordtype,
267-
_from_timeparts,
268-
_until_timeparts,
255+
prefix=prefix,
256+
recordtype=recordtype,
257+
from_timeparts=_whenparts(from_when, timedepth),
258+
until_timeparts=_whenparts(until_when, timedepth),
269259
timedepth=timedepth,
270260
include_less_granular=include_less_granular,
271261
only_datelike=True,
@@ -365,7 +355,7 @@ def _parse_timename(given_timename: str) -> Iterator[int]:
365355
yield int(_part)
366356

367357

368-
def timename_from_datestr(given_date: str, part_count: int) -> str:
358+
def timename_from_datestr(given_date: str, timedepth: int) -> str:
369359
"""
370360
>>> timename_from_datestr('2345-06-07', 1)
371361
'2345'
@@ -384,28 +374,32 @@ def timename_from_datestr(given_date: str, part_count: int) -> str:
384374
"""
385375
return timename_from_date(
386376
datetime.datetime.fromisoformat(given_date),
387-
part_count=part_count,
377+
timedepth=timedepth,
388378
)
389379

390380

391-
def timename_from_date(given_date: datetime.date, part_count: int) -> str:
381+
def timename_from_date(given_date: datetime.date, timedepth: int) -> str:
392382
"""
393383
>>> timename_from_date(datetime.date(3456, 7, 8), 2)
394384
'3456_07'
395385
"""
396-
_timeparts = itertools.islice(_each_timepart_from_date(given_date), part_count)
386+
_timeparts = itertools.islice(_each_timepart_from_date(given_date), timedepth)
397387
return _format_timename(*_timeparts)
398388

399389

400-
def timeparts_from_date(given_date: datetime.date, part_count: int) -> tuple[int, ...]:
390+
def timeparts_from_date(given_date: datetime.date, timedepth: int) -> tuple[int, ...]:
401391
"""
402392
>>> timeparts_from_date(datetime.date(3456, 7, 8), 2)
403393
(3456, 7)
404394
>>> timeparts_from_date(datetime.date(3456, 7, 8), 4)
405395
(3456, 7, 8, 0)
406396
"""
407-
_parts = itertools.chain(_each_timepart_from_date(given_date), itertools.repeat(0))
408-
return tuple(itertools.islice(_parts, part_count))
397+
return tuple(
398+
itertools.islice(
399+
_zeropadded_timeparts(_each_timepart_from_date(given_date)),
400+
timedepth,
401+
)
402+
)
409403

410404

411405
def _each_timepart_from_date(given_date: datetime.date) -> Iterator[int]:
@@ -418,5 +412,66 @@ def _each_timepart_from_date(given_date: datetime.date) -> Iterator[int]:
418412
yield given_date.second
419413

420414

415+
def _zeropadded_timeparts(
416+
timeparts: collections.abc.Iterable[int],
417+
) -> collections.abc.Iterator[int]:
418+
yield from timeparts
419+
yield from itertools.repeat(0)
420+
421+
421422
def _format_timepart(timepart: int) -> str:
422423
return f"{timepart:0{_TIMEPART_MIN_LEN}}"
424+
425+
426+
def _whenparts(
427+
when: tuple[int, ...] | datetime.date,
428+
timedepth: int,
429+
) -> tuple[int, ...]:
430+
return (
431+
timeparts_from_date(when, timedepth)
432+
if isinstance(when, datetime.date)
433+
else tuple(itertools.islice(_zeropadded_timeparts(when), timedepth))
434+
)
435+
436+
437+
def full_semverlike_timeparts(when: tuple[int, ...] | datetime.date) -> str:
438+
"""
439+
>>> full_semverlike_timeparts((3000, 2))
440+
'3000.2'
441+
>>> full_semverlike_timeparts(datetime.date(3000, 7, 12))
442+
'3000.7.12'
443+
>>> full_semverlike_timeparts(datetime.datetime(3000, 9, 1))
444+
'3000.9.1.0.0.0'
445+
"""
446+
_parts = (
447+
timeparts_from_date(
448+
when, timedepth=(6 if isinstance(when, datetime.datetime) else 3)
449+
)
450+
if isinstance(when, datetime.date)
451+
else when
452+
)
453+
return ".".join(map(str, _parts))
454+
455+
456+
def semverlike_timeparts(when: tuple[int, ...] | datetime.date, timedepth: int) -> str:
457+
"""
458+
>>> semverlike_timeparts((3000, 2, 7, 9), timedepth=2)
459+
'3000.2'
460+
>>> semverlike_timeparts((3000, 2), timedepth=1)
461+
'3000'
462+
>>> semverlike_timeparts((3000, 2, 7, 9), timedepth=5)
463+
'3000.2.7.9.0'
464+
465+
>>> semverlike_timeparts(datetime.date(3000, 7, 12), timedepth=3)
466+
'3000.7.12'
467+
>>> semverlike_timeparts(datetime.date(3000, 7, 12), timedepth=2)
468+
'3000.7'
469+
>>> semverlike_timeparts(datetime.date(3000, 7, 12), timedepth=1)
470+
'3000'
471+
472+
>>> semverlike_timeparts(datetime.datetime(3000, 9, 1, 5, 2), timedepth=6)
473+
'3000.9.1.5.2.0'
474+
>>> semverlike_timeparts(datetime.datetime(3000, 9, 1, 5, 2), timedepth=3)
475+
'3000.9.1'
476+
"""
477+
return full_semverlike_timeparts(_whenparts(when, timedepth))

0 commit comments

Comments
 (0)