dycw-utilities 0.146.2__py3-none-any.whl → 0.178.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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (89) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +129 -50
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +33 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +131 -93
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/packaging.py +115 -0
  39. utilities/parse.py +2 -2
  40. utilities/pathlib.py +66 -34
  41. utilities/permissions.py +298 -0
  42. utilities/platform.py +5 -4
  43. utilities/polars.py +934 -420
  44. utilities/polars_ols.py +1 -1
  45. utilities/postgres.py +317 -153
  46. utilities/pottery.py +10 -86
  47. utilities/pqdm.py +3 -3
  48. utilities/pwd.py +28 -0
  49. utilities/pydantic.py +4 -51
  50. utilities/pydantic_settings.py +240 -0
  51. utilities/pydantic_settings_sops.py +76 -0
  52. utilities/pyinstrument.py +5 -5
  53. utilities/pytest.py +100 -126
  54. utilities/pytest_plugins/pytest_randomly.py +1 -1
  55. utilities/pytest_plugins/pytest_regressions.py +7 -3
  56. utilities/pytest_regressions.py +27 -8
  57. utilities/random.py +11 -6
  58. utilities/re.py +1 -1
  59. utilities/redis.py +101 -64
  60. utilities/sentinel.py +10 -0
  61. utilities/shelve.py +4 -1
  62. utilities/shutil.py +25 -0
  63. utilities/slack_sdk.py +9 -4
  64. utilities/sqlalchemy.py +422 -352
  65. utilities/sqlalchemy_polars.py +28 -52
  66. utilities/string.py +1 -1
  67. utilities/subprocess.py +1977 -0
  68. utilities/tempfile.py +112 -4
  69. utilities/testbook.py +50 -0
  70. utilities/text.py +174 -42
  71. utilities/throttle.py +158 -0
  72. utilities/timer.py +2 -2
  73. utilities/traceback.py +59 -38
  74. utilities/types.py +68 -22
  75. utilities/typing.py +479 -19
  76. utilities/uuid.py +42 -5
  77. utilities/version.py +27 -26
  78. utilities/whenever.py +663 -178
  79. utilities/zoneinfo.py +80 -22
  80. dycw_utilities-0.146.2.dist-info/METADATA +0 -41
  81. dycw_utilities-0.146.2.dist-info/RECORD +0 -99
  82. dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
  83. dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
  84. utilities/aiolimiter.py +0 -25
  85. utilities/eventkit.py +0 -388
  86. utilities/period.py +0 -237
  87. utilities/python_dotenv.py +0 -101
  88. utilities/streamlit.py +0 -105
  89. utilities/typed_settings.py +0 -144
utilities/eventkit.py DELETED
@@ -1,388 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from functools import wraps
5
- from inspect import iscoroutinefunction
6
- from typing import TYPE_CHECKING, Any, Self, assert_never, cast, override
7
-
8
- from eventkit import (
9
- Constant,
10
- Count,
11
- DropWhile,
12
- Enumerate,
13
- Event,
14
- Filter,
15
- Fork,
16
- Iterate,
17
- Map,
18
- Pack,
19
- Partial,
20
- PartialRight,
21
- Pluck,
22
- Skip,
23
- Star,
24
- Take,
25
- TakeUntil,
26
- TakeWhile,
27
- Timestamp,
28
- )
29
-
30
- from utilities.functions import apply_decorators
31
- from utilities.iterables import always_iterable
32
- from utilities.logging import get_logger
33
-
34
- if TYPE_CHECKING:
35
- from collections.abc import Callable
36
-
37
- from utilities.types import Coro, LoggerOrName, MaybeCoro, MaybeIterable, TypeLike
38
-
39
-
40
- ##
41
-
42
-
43
- def add_listener[E: Event, F: Callable](
44
- event: E,
45
- listener: Callable[..., MaybeCoro[None]],
46
- /,
47
- *,
48
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
49
- ignore: TypeLike[BaseException] | None = None,
50
- logger: LoggerOrName | None = None,
51
- decorators: MaybeIterable[Callable[[F], F]] | None = None,
52
- done: Callable[..., MaybeCoro[None]] | None = None,
53
- keep_ref: bool = False,
54
- ) -> E:
55
- """Connect a listener to an event."""
56
- lifted = lift_listener(
57
- listener,
58
- event,
59
- error=error,
60
- ignore=ignore,
61
- logger=logger,
62
- decorators=decorators,
63
- )
64
- return cast("E", event.connect(lifted, done=done, keep_ref=keep_ref))
65
-
66
-
67
- ##
68
-
69
-
70
- @dataclass(repr=False, kw_only=True)
71
- class LiftedEvent[F: Callable[..., MaybeCoro[None]]]:
72
- """A lifted version of `Event`."""
73
-
74
- event: Event
75
-
76
- def name(self) -> str:
77
- return self.event.name() # pragma: no cover
78
-
79
- def done(self) -> bool:
80
- return self.event.done() # pragma: no cover
81
-
82
- def set_done(self) -> None:
83
- self.event.set_done() # pragma: no cover
84
-
85
- def value(self) -> Any:
86
- return self.event.value() # pragma: no cover
87
-
88
- def connect[F2: Callable](
89
- self,
90
- listener: F,
91
- /,
92
- *,
93
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
94
- ignore: TypeLike[BaseException] | None = None,
95
- logger: LoggerOrName | None = None,
96
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
97
- done: Callable[..., MaybeCoro[None]] | None = None,
98
- keep_ref: bool = False,
99
- ) -> Event:
100
- return add_listener(
101
- self.event,
102
- listener,
103
- error=error,
104
- ignore=ignore,
105
- logger=logger,
106
- decorators=decorators,
107
- done=done,
108
- keep_ref=keep_ref,
109
- )
110
-
111
- def disconnect(
112
- self, listener: Any, /, *, error: Any = None, done: Any = None
113
- ) -> Any:
114
- return self.event.disconnect( # pragma: no cover
115
- listener, error=error, done=done
116
- )
117
-
118
- def disconnect_obj(self, obj: Any, /) -> None:
119
- self.event.disconnect_obj(obj) # pragma: no cover
120
-
121
- def emit(self, *args: Any) -> None:
122
- self.event.emit(*args) # pragma: no cover
123
-
124
- def emit_threadsafe(self, *args: Any) -> None:
125
- self.event.emit_threadsafe(*args) # pragma: no cover
126
-
127
- def clear(self) -> None:
128
- self.event.clear() # pragma: no cover
129
-
130
- def run(self) -> list[Any]:
131
- return self.event.run() # pragma: no cover
132
-
133
- def pipe(self, *targets: Event) -> Event:
134
- return self.event.pipe(*targets) # pragma: no cover
135
-
136
- def fork(self, *targets: Event) -> Fork:
137
- return self.event.fork(*targets) # pragma: no cover
138
-
139
- def set_source(self, source: Any, /) -> None:
140
- self.event.set_source(source) # pragma: no cover
141
-
142
- def _onFinalize(self, ref: Any) -> None: # noqa: N802
143
- self.event._onFinalize(ref) # noqa: SLF001 # pragma: no cover
144
-
145
- async def aiter(self, *, skip_to_last: bool = False, tuples: bool = False) -> Any:
146
- async for i in self.event.aiter( # pragma: no cover
147
- skip_to_last=skip_to_last, tuples=tuples
148
- ):
149
- yield i
150
-
151
- __iadd__ = connect
152
- __isub__ = disconnect
153
- __call__ = emit
154
- __or__ = pipe
155
-
156
- @override
157
- def __repr__(self) -> str:
158
- return self.event.__repr__() # pragma: no cover
159
-
160
- def __len__(self) -> int:
161
- return self.event.__len__() # pragma: no cover
162
-
163
- def __bool__(self) -> bool:
164
- return self.event.__bool__() # pragma: no cover
165
-
166
- def __getitem__(self, fork_targets: Any, /) -> Fork:
167
- return self.event.__getitem__(fork_targets) # pragma: no cover
168
-
169
- def __await__(self) -> Any:
170
- return self.event.__await__() # pragma: no cover
171
-
172
- __aiter__ = aiter
173
-
174
- def __contains__(self, c: Any, /) -> bool:
175
- return self.event.__contains__(c) # pragma: no cover
176
-
177
- @override
178
- def __reduce__(self) -> Any:
179
- return self.event.__reduce__() # pragma: no cover
180
-
181
- def filter(self, *, predicate: Any = bool) -> Filter:
182
- return self.event.filter(predicate=predicate) # pragma: no cover
183
-
184
- def skip(self, *, count: int = 1) -> Skip:
185
- return self.event.skip(count=count) # pragma: no cover
186
-
187
- def take(self, *, count: int = 1) -> Take:
188
- return self.event.take(count=count) # pragma: no cover
189
-
190
- def takewhile(self, *, predicate: Any = bool) -> TakeWhile:
191
- return self.event.takewhile(predicate=predicate) # pragma: no cover
192
-
193
- def dropwhile(self, *, predicate: Any = lambda x: not x) -> DropWhile: # pyright: ignore[reportUnknownLambdaType]
194
- return self.event.dropwhile(predicate=predicate) # pragma: no cover
195
-
196
- def takeuntil(self, notifier: Event, /) -> TakeUntil:
197
- return self.event.takeuntil(notifier) # pragma: no cover
198
-
199
- def constant(self, constant: Any, /) -> Constant:
200
- return self.event.constant(constant) # pragma: no cover
201
-
202
- def iterate(self, it: Any, /) -> Iterate:
203
- return self.event.iterate(it) # pragma: no cover
204
-
205
- def count(self, *, start: int = 0, step: int = 1) -> Count:
206
- return self.event.count(start=start, step=step) # pragma: no cover
207
-
208
- def enumerate(self, *, start: int = 0, step: int = 1) -> Enumerate:
209
- return self.event.enumerate(start=start, step=step) # pragma: no cover
210
-
211
- def timestamp(self) -> Timestamp:
212
- return self.event.timestamp() # pragma: no cover
213
-
214
- def partial(self, *left_args: Any) -> Partial:
215
- return self.event.partial(*left_args) # pragma: no cover
216
-
217
- def partial_right(self, *right_args: Any) -> PartialRight:
218
- return self.event.partial_right(*right_args) # pragma: no cover
219
-
220
- def star(self) -> Star:
221
- return self.event.star() # pragma: no cover
222
-
223
- def pack(self) -> Pack:
224
- return self.event.pack() # pragma: no cover
225
-
226
- def pluck(self, *selections: int | str) -> Pluck:
227
- return self.event.pluck(*selections) # pragma: no cover
228
-
229
- def map(
230
- self,
231
- func: Any,
232
- /,
233
- *,
234
- timeout: float | None = None,
235
- ordered: bool = True,
236
- task_limit: int | None = None,
237
- ) -> Map:
238
- return self.event.map( # pragma: no cover
239
- func, timeout=timeout, ordered=ordered, task_limit=task_limit
240
- )
241
-
242
-
243
- ##
244
-
245
-
246
- class TypedEvent[F: Callable[..., MaybeCoro[None]]](Event):
247
- """A typed version of `Event`."""
248
-
249
- @override
250
- def connect[F2: Callable](
251
- self,
252
- listener: F,
253
- error: Callable[[Self, BaseException], MaybeCoro[None]] | None = None,
254
- done: Callable[[Self], MaybeCoro[None]] | None = None,
255
- keep_ref: bool = False,
256
- *,
257
- ignore: TypeLike[BaseException] | None = None,
258
- logger: LoggerOrName | None = None,
259
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
260
- ) -> Self:
261
- lifted = lift_listener(
262
- listener,
263
- self,
264
- error=cast(
265
- "Callable[[Event, BaseException], MaybeCoro[None]] | None", error
266
- ),
267
- ignore=ignore,
268
- logger=logger,
269
- decorators=decorators,
270
- )
271
- return cast(
272
- "Self", super().connect(lifted, error=error, done=done, keep_ref=keep_ref)
273
- )
274
-
275
-
276
- ##
277
-
278
-
279
- def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
280
- listener: F1,
281
- event: Event,
282
- /,
283
- *,
284
- error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
285
- ignore: TypeLike[BaseException] | None = None,
286
- logger: LoggerOrName | None = None,
287
- decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
288
- ) -> F1:
289
- match error, bool(iscoroutinefunction(listener)):
290
- case None, False:
291
- listener_typed = cast("Callable[..., None]", listener)
292
-
293
- @wraps(listener)
294
- def listener_no_error_sync(*args: Any, **kwargs: Any) -> None:
295
- try:
296
- listener_typed(*args, **kwargs)
297
- except Exception as exc: # noqa: BLE001
298
- if (ignore is not None) and isinstance(exc, ignore):
299
- return
300
- get_logger(logger=logger).exception("")
301
-
302
- lifted = listener_no_error_sync
303
-
304
- case None, True:
305
- listener_typed = cast("Callable[..., Coro[None]]", listener)
306
-
307
- @wraps(listener)
308
- async def listener_no_error_async(*args: Any, **kwargs: Any) -> None:
309
- try:
310
- await listener_typed(*args, **kwargs)
311
- except Exception as exc: # noqa: BLE001
312
- if (ignore is not None) and isinstance(exc, ignore):
313
- return
314
- get_logger(logger=logger).exception("")
315
-
316
- lifted = listener_no_error_async
317
- case _, _:
318
- match bool(iscoroutinefunction(listener)), bool(iscoroutinefunction(error)):
319
- case False, False:
320
- listener_typed = cast("Callable[..., None]", listener)
321
- error_typed = cast("Callable[[Event, Exception], None]", error)
322
-
323
- @wraps(listener)
324
- def listener_have_error_sync(*args: Any, **kwargs: Any) -> None:
325
- try:
326
- listener_typed(*args, **kwargs)
327
- except Exception as exc: # noqa: BLE001
328
- if (ignore is not None) and isinstance(exc, ignore):
329
- return
330
- error_typed(event, exc)
331
-
332
- lifted = listener_have_error_sync
333
- case False, True:
334
- listener_typed = cast("Callable[..., None]", listener)
335
- error_typed = cast(
336
- "Callable[[Event, Exception], Coro[None]]", error
337
- )
338
- raise LiftListenerError(listener=listener_typed, error=error_typed)
339
- case True, _:
340
- listener_typed = cast("Callable[..., Coro[None]]", listener)
341
-
342
- @wraps(listener)
343
- async def listener_have_error_async(
344
- *args: Any, **kwargs: Any
345
- ) -> None:
346
- try:
347
- await listener_typed(*args, **kwargs)
348
- except Exception as exc: # noqa: BLE001
349
- if (ignore is not None) and isinstance(exc, ignore):
350
- return None
351
- if iscoroutinefunction(error):
352
- error_typed = cast(
353
- "Callable[[Event, Exception], Coro[None]]", error
354
- )
355
- return await error_typed(event, exc)
356
- error_typed = cast(
357
- "Callable[[Event, Exception], None]", error
358
- )
359
- error_typed(event, exc)
360
-
361
- lifted = listener_have_error_async
362
- case _ as never:
363
- assert_never(never)
364
- case _ as never:
365
- assert_never(never)
366
-
367
- if decorators is not None:
368
- lifted = apply_decorators(lifted, *always_iterable(decorators))
369
- return cast("F1", lifted)
370
-
371
-
372
- @dataclass(kw_only=True, slots=True)
373
- class LiftListenerError(Exception):
374
- listener: Callable[..., None]
375
- error: Callable[[Event, Exception], Coro[None]]
376
-
377
- @override
378
- def __str__(self) -> str:
379
- return f"Synchronous listener {self.listener} cannot be paired with an asynchronous error handler {self.error}"
380
-
381
-
382
- __all__ = [
383
- "LiftListenerError",
384
- "LiftedEvent",
385
- "TypedEvent",
386
- "add_listener",
387
- "lift_listener",
388
- ]
utilities/period.py DELETED
@@ -1,237 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Any, Self, TypedDict, overload, override
5
- from zoneinfo import ZoneInfo
6
-
7
- from whenever import Date, DateDelta, PlainDateTime, TimeDelta, ZonedDateTime
8
-
9
- from utilities.dataclasses import replace_non_sentinel
10
- from utilities.functions import get_class_name
11
- from utilities.sentinel import Sentinel, sentinel
12
- from utilities.whenever import format_compact
13
- from utilities.zoneinfo import get_time_zone_name
14
-
15
- if TYPE_CHECKING:
16
- from utilities.types import TimeZoneLike
17
-
18
-
19
- class _PeriodAsDict[T: (Date, ZonedDateTime)](TypedDict):
20
- start: T
21
- end: T
22
-
23
-
24
- @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
25
- class DatePeriod:
26
- """A period of dates."""
27
-
28
- start: Date
29
- end: Date
30
-
31
- def __post_init__(self) -> None:
32
- if self.start > self.end:
33
- raise _PeriodInvalidError(start=self.start, end=self.end)
34
-
35
- def __add__(self, other: DateDelta, /) -> Self:
36
- """Offset the period."""
37
- return self.replace(start=self.start + other, end=self.end + other)
38
-
39
- def __contains__(self, other: Date, /) -> bool:
40
- """Check if a date/datetime lies in the period."""
41
- return self.start <= other <= self.end
42
-
43
- @override
44
- def __repr__(self) -> str:
45
- cls = get_class_name(self)
46
- return f"{cls}({self.start}, {self.end})"
47
-
48
- def __sub__(self, other: DateDelta, /) -> Self:
49
- """Offset the period."""
50
- return self.replace(start=self.start - other, end=self.end - other)
51
-
52
- @property
53
- def delta(self) -> DateDelta:
54
- """The delta of the period."""
55
- return self.end - self.start
56
-
57
- def format_compact(self) -> str:
58
- """Format the period in a compact fashion."""
59
- fc, start, end = format_compact, self.start, self.end
60
- if self.start == self.end:
61
- return f"{fc(start)}="
62
- if self.start.year_month() == self.end.year_month():
63
- return f"{fc(start)}-{fc(end, fmt='%d')}"
64
- if self.start.year == self.end.year:
65
- return f"{fc(start)}-{fc(end, fmt='%m%d')}"
66
- return f"{fc(start)}-{fc(end)}"
67
-
68
- def replace(
69
- self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
70
- ) -> Self:
71
- """Replace elements of the period."""
72
- return replace_non_sentinel(self, start=start, end=end)
73
-
74
- def to_dict(self) -> _PeriodAsDict[Date]:
75
- """Convert the period to a dictionary."""
76
- return _PeriodAsDict(start=self.start, end=self.end)
77
-
78
-
79
- @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
80
- class ZonedDateTimePeriod:
81
- """A period of time."""
82
-
83
- start: ZonedDateTime
84
- end: ZonedDateTime
85
-
86
- def __post_init__(self) -> None:
87
- if self.start > self.end:
88
- raise _PeriodInvalidError(start=self.start, end=self.end)
89
- if self.start.tz != self.end.tz:
90
- raise _PeriodTimeZoneError(
91
- start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
92
- )
93
-
94
- def __add__(self, other: TimeDelta, /) -> Self:
95
- """Offset the period."""
96
- return self.replace(start=self.start + other, end=self.end + other)
97
-
98
- def __contains__(self, other: ZonedDateTime, /) -> bool:
99
- """Check if a date/datetime lies in the period."""
100
- return self.start <= other <= self.end
101
-
102
- @override
103
- def __repr__(self) -> str:
104
- cls = get_class_name(self)
105
- return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
106
-
107
- def __sub__(self, other: TimeDelta, /) -> Self:
108
- """Offset the period."""
109
- return self.replace(start=self.start - other, end=self.end - other)
110
-
111
- @property
112
- def delta(self) -> TimeDelta:
113
- """The duration of the period."""
114
- return self.end - self.start
115
-
116
- @overload
117
- def exact_eq(self, period: ZonedDateTimePeriod, /) -> bool: ...
118
- @overload
119
- def exact_eq(self, start: ZonedDateTime, end: ZonedDateTime, /) -> bool: ...
120
- @overload
121
- def exact_eq(
122
- self, start: PlainDateTime, end: PlainDateTime, time_zone: ZoneInfo, /
123
- ) -> bool: ...
124
- def exact_eq(self, *args: Any) -> bool:
125
- """Check if a period is exactly equal to another."""
126
- if (len(args) == 1) and isinstance(args[0], ZonedDateTimePeriod):
127
- return self.start.exact_eq(args[0].start) and self.end.exact_eq(args[0].end)
128
- if (
129
- (len(args) == 2)
130
- and isinstance(args[0], ZonedDateTime)
131
- and isinstance(args[1], ZonedDateTime)
132
- ):
133
- return self.exact_eq(ZonedDateTimePeriod(args[0], args[1]))
134
- if (
135
- (len(args) == 3)
136
- and isinstance(args[0], PlainDateTime)
137
- and isinstance(args[1], PlainDateTime)
138
- and isinstance(args[2], ZoneInfo)
139
- ):
140
- return self.exact_eq(
141
- ZonedDateTimePeriod(
142
- args[0].assume_tz(args[2].key), args[1].assume_tz(args[2].key)
143
- )
144
- )
145
- raise _PeriodExactEqArgumentsError(args=args)
146
-
147
- def format_compact(self) -> str:
148
- """Format the period in a compact fashion."""
149
- fc, start, end = format_compact, self.start, self.end
150
- if start == end:
151
- if end.second != 0:
152
- return f"{fc(start)}="
153
- if end.minute != 0:
154
- return f"{fc(start, fmt='%Y%m%dT%H%M')}="
155
- return f"{fc(start, fmt='%Y%m%dT%H')}="
156
- if start.date() == end.date():
157
- if end.second != 0:
158
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M%S')}"
159
- if end.minute != 0:
160
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M')}"
161
- return f"{fc(start.to_plain())}-{fc(end, fmt='%H')}"
162
- if start.date().year_month() == end.date().year_month():
163
- if end.second != 0:
164
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M%S')}"
165
- if end.minute != 0:
166
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M')}"
167
- return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H')}"
168
- if start.year == end.year:
169
- if end.second != 0:
170
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M%S')}"
171
- if end.minute != 0:
172
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M')}"
173
- return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H')}"
174
- if end.second != 0:
175
- return f"{fc(start.to_plain())}-{fc(end)}"
176
- if end.minute != 0:
177
- return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H%M')}"
178
- return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H')}"
179
-
180
- def replace(
181
- self,
182
- *,
183
- start: ZonedDateTime | Sentinel = sentinel,
184
- end: ZonedDateTime | Sentinel = sentinel,
185
- ) -> Self:
186
- """Replace elements of the period."""
187
- return replace_non_sentinel(self, start=start, end=end)
188
-
189
- @property
190
- def time_zone(self) -> ZoneInfo:
191
- """The time zone of the period."""
192
- return ZoneInfo(self.start.tz)
193
-
194
- def to_dict(self) -> _PeriodAsDict[ZonedDateTime]:
195
- """Convert the period to a dictionary."""
196
- return _PeriodAsDict(start=self.start, end=self.end)
197
-
198
- def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
199
- """Convert the time zone."""
200
- tz = get_time_zone_name(time_zone)
201
- return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
202
-
203
-
204
- @dataclass(kw_only=True, slots=True)
205
- class PeriodError(Exception): ...
206
-
207
-
208
- @dataclass(kw_only=True, slots=True)
209
- class _PeriodInvalidError[T: (Date, ZonedDateTime)](PeriodError):
210
- start: T
211
- end: T
212
-
213
- @override
214
- def __str__(self) -> str:
215
- return f"Invalid period; got {self.start} > {self.end}"
216
-
217
-
218
- @dataclass(kw_only=True, slots=True)
219
- class _PeriodTimeZoneError(PeriodError):
220
- start: ZoneInfo
221
- end: ZoneInfo
222
-
223
- @override
224
- def __str__(self) -> str:
225
- return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
226
-
227
-
228
- @dataclass(kw_only=True, slots=True)
229
- class _PeriodExactEqArgumentsError(PeriodError):
230
- args: tuple[Any, ...]
231
-
232
- @override
233
- def __str__(self) -> str:
234
- return f"Invalid arguments; got {self.args}"
235
-
236
-
237
- __all__ = ["DatePeriod", "PeriodError", "ZonedDateTimePeriod"]