minimindslab-datetime-span 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MiniMindsLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: minimindslab-datetime-span
3
+ Version: 0.1.0
4
+ Summary: Parse, normalize, and compare simple UTC datetime spans with deterministic formatting.
5
+ Author: MiniMindsLab
6
+ License: MIT
7
+ Keywords: datetime,timespan,utc,iso8601,cli
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # MiniMindsLab DateTime Span
22
+
23
+ `minimindslab-datetime-span` is a tiny, pure-Python utility for parsing, validating, formatting, and comparing UTC datetime spans.
24
+
25
+ ## Features
26
+
27
+ - Parse UTC ISO-8601 datetime strings (`Z` or `+00:00`).
28
+ - Model spans with inclusive/exclusive boundaries.
29
+ - Deterministic canonical formatting.
30
+ - Compare spans for overlap, containment, and adjacency.
31
+ - CLI that reads from a file or stdin and prints canonicalized spans.
32
+
33
+ ## Span Format
34
+
35
+ Library and CLI accept bracketed span lines:
36
+
37
+ - `[START,END]` inclusive start and end
38
+ - `(START,END)` exclusive start and end
39
+ - `[START,END)` inclusive start, exclusive end
40
+ - `(START,END]` exclusive start, inclusive end
41
+
42
+ Datetimes must be UTC and ISO-like (e.g. `2024-01-02T03:04:05Z`).
43
+
44
+ Canonical output uses second precision and `Z`, e.g.:
45
+
46
+ `[2024-01-02T03:04:05Z,2024-01-02T04:04:05Z)`
47
+
48
+ ## CLI
49
+
50
+ ```bash
51
+ # From a file
52
+ miniminds-datetime-span --file spans.txt
53
+
54
+ # From stdin
55
+ printf '[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)\n' | miniminds-datetime-span
56
+ ```
57
+
58
+ Each non-empty, non-comment line (`# ...`) is parsed and canonicalized.
@@ -0,0 +1,38 @@
1
+ # MiniMindsLab DateTime Span
2
+
3
+ `minimindslab-datetime-span` is a tiny, pure-Python utility for parsing, validating, formatting, and comparing UTC datetime spans.
4
+
5
+ ## Features
6
+
7
+ - Parse UTC ISO-8601 datetime strings (`Z` or `+00:00`).
8
+ - Model spans with inclusive/exclusive boundaries.
9
+ - Deterministic canonical formatting.
10
+ - Compare spans for overlap, containment, and adjacency.
11
+ - CLI that reads from a file or stdin and prints canonicalized spans.
12
+
13
+ ## Span Format
14
+
15
+ Library and CLI accept bracketed span lines:
16
+
17
+ - `[START,END]` inclusive start and end
18
+ - `(START,END)` exclusive start and end
19
+ - `[START,END)` inclusive start, exclusive end
20
+ - `(START,END]` exclusive start, inclusive end
21
+
22
+ Datetimes must be UTC and ISO-like (e.g. `2024-01-02T03:04:05Z`).
23
+
24
+ Canonical output uses second precision and `Z`, e.g.:
25
+
26
+ `[2024-01-02T03:04:05Z,2024-01-02T04:04:05Z)`
27
+
28
+ ## CLI
29
+
30
+ ```bash
31
+ # From a file
32
+ miniminds-datetime-span --file spans.txt
33
+
34
+ # From stdin
35
+ printf '[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)\n' | miniminds-datetime-span
36
+ ```
37
+
38
+ Each non-empty, non-comment line (`# ...`) is parsed and canonicalized.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "minimindslab-datetime-span"
7
+ version = "0.1.0"
8
+ description = "Parse, normalize, and compare simple UTC datetime spans with deterministic formatting."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "MiniMindsLab" }]
13
+ dependencies = []
14
+ keywords = ["datetime", "timespan", "utc", "iso8601", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Libraries",
23
+ "Topic :: Utilities"
24
+ ]
25
+
26
+ [project.scripts]
27
+ miniminds-datetime-span = "minimindslab_datetime_span.__main__:main"
28
+
29
+ [tool.setuptools]
30
+ package-dir = {"" = "src"}
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """MiniMindsLab DateTime Span library."""
2
+
3
+ from .core import (
4
+ DateTimeSpan,
5
+ SpanParseError,
6
+ parse_utc_datetime,
7
+ parse_span,
8
+ )
9
+
10
+ __all__ = [
11
+ "DateTimeSpan",
12
+ "SpanParseError",
13
+ "parse_utc_datetime",
14
+ "parse_span",
15
+ ]
16
+
17
+ __version__ = "0.1.0"
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .core import SpanParseError, parse_span
7
+
8
+
9
+ def _iter_lines(path: str | None):
10
+ if path:
11
+ with open(path, "r", encoding="utf-8") as f:
12
+ for line in f:
13
+ yield line
14
+ else:
15
+ for line in sys.stdin:
16
+ yield line
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = argparse.ArgumentParser(description="Canonicalize UTC datetime spans.")
21
+ parser.add_argument("--file", "-f", dest="file", help="Input text file; defaults to stdin")
22
+ args = parser.parse_args(argv)
23
+
24
+ try:
25
+ for raw in _iter_lines(args.file):
26
+ line = raw.strip()
27
+ if not line or line.startswith("#"):
28
+ continue
29
+ print(parse_span(line).canonical())
30
+ except (OSError, SpanParseError) as exc:
31
+ print(f"error: {exc}", file=sys.stderr)
32
+ return 2
33
+ return 0
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+
7
+ class SpanParseError(ValueError):
8
+ """Raised when datetime or span parsing/validation fails."""
9
+
10
+
11
+ def parse_utc_datetime(value: str) -> datetime:
12
+ s = value.strip()
13
+ if s.endswith("Z"):
14
+ s = s[:-1] + "+00:00"
15
+ try:
16
+ dt = datetime.fromisoformat(s)
17
+ except ValueError as exc:
18
+ raise SpanParseError(f"Invalid ISO-8601 datetime: {value!r}") from exc
19
+ if dt.tzinfo is None:
20
+ raise SpanParseError(f"Datetime must include UTC timezone: {value!r}")
21
+ if dt.utcoffset() != timedelta(0):
22
+ raise SpanParseError(f"Datetime must be UTC (+00:00 or Z): {value!r}")
23
+ return dt.astimezone(timezone.utc)
24
+
25
+
26
+ def _fmt_utc(dt: datetime) -> str:
27
+ return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class DateTimeSpan:
32
+ start: datetime
33
+ end: datetime
34
+ start_inclusive: bool = True
35
+ end_inclusive: bool = False
36
+
37
+ def __post_init__(self) -> None:
38
+ s = self.start.astimezone(timezone.utc)
39
+ e = self.end.astimezone(timezone.utc)
40
+ object.__setattr__(self, "start", s)
41
+ object.__setattr__(self, "end", e)
42
+ if e < s:
43
+ raise SpanParseError("Invalid span: end precedes start")
44
+
45
+ def canonical(self) -> str:
46
+ l = "[" if self.start_inclusive else "("
47
+ r = "]" if self.end_inclusive else ")"
48
+ return f"{l}{_fmt_utc(self.start)},{_fmt_utc(self.end)}{r}"
49
+
50
+ def overlaps(self, other: "DateTimeSpan") -> bool:
51
+ if self.end < other.start or other.end < self.start:
52
+ return False
53
+ if self.end == other.start:
54
+ return self.end_inclusive and other.start_inclusive
55
+ if other.end == self.start:
56
+ return other.end_inclusive and self.start_inclusive
57
+ return True
58
+
59
+ def contains(self, other: "DateTimeSpan") -> bool:
60
+ left_ok = self.start < other.start or (self.start == other.start and (self.start_inclusive or not other.start_inclusive))
61
+ right_ok = self.end > other.end or (self.end == other.end and (self.end_inclusive or not other.end_inclusive))
62
+ return left_ok and right_ok
63
+
64
+ def is_adjacent(self, other: "DateTimeSpan") -> bool:
65
+ if self.end == other.start:
66
+ return not (self.end_inclusive and other.start_inclusive)
67
+ if other.end == self.start:
68
+ return not (other.end_inclusive and self.start_inclusive)
69
+ return False
70
+
71
+
72
+ def parse_span(text: str) -> DateTimeSpan:
73
+ s = text.strip()
74
+ if len(s) < 5:
75
+ raise SpanParseError("Invalid span syntax")
76
+ if s[0] not in "([" or s[-1] not in ")]":
77
+ raise SpanParseError("Span must start with '(' or '[' and end with ')' or ']'")
78
+ body = s[1:-1]
79
+ parts = body.split(",")
80
+ if len(parts) != 2:
81
+ raise SpanParseError("Span must contain exactly one comma separating start and end")
82
+ start = parse_utc_datetime(parts[0])
83
+ end = parse_utc_datetime(parts[1])
84
+ return DateTimeSpan(start, end, start_inclusive=(s[0] == "["), end_inclusive=(s[-1] == "]"))
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: minimindslab-datetime-span
3
+ Version: 0.1.0
4
+ Summary: Parse, normalize, and compare simple UTC datetime spans with deterministic formatting.
5
+ Author: MiniMindsLab
6
+ License: MIT
7
+ Keywords: datetime,timespan,utc,iso8601,cli
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # MiniMindsLab DateTime Span
22
+
23
+ `minimindslab-datetime-span` is a tiny, pure-Python utility for parsing, validating, formatting, and comparing UTC datetime spans.
24
+
25
+ ## Features
26
+
27
+ - Parse UTC ISO-8601 datetime strings (`Z` or `+00:00`).
28
+ - Model spans with inclusive/exclusive boundaries.
29
+ - Deterministic canonical formatting.
30
+ - Compare spans for overlap, containment, and adjacency.
31
+ - CLI that reads from a file or stdin and prints canonicalized spans.
32
+
33
+ ## Span Format
34
+
35
+ Library and CLI accept bracketed span lines:
36
+
37
+ - `[START,END]` inclusive start and end
38
+ - `(START,END)` exclusive start and end
39
+ - `[START,END)` inclusive start, exclusive end
40
+ - `(START,END]` exclusive start, inclusive end
41
+
42
+ Datetimes must be UTC and ISO-like (e.g. `2024-01-02T03:04:05Z`).
43
+
44
+ Canonical output uses second precision and `Z`, e.g.:
45
+
46
+ `[2024-01-02T03:04:05Z,2024-01-02T04:04:05Z)`
47
+
48
+ ## CLI
49
+
50
+ ```bash
51
+ # From a file
52
+ miniminds-datetime-span --file spans.txt
53
+
54
+ # From stdin
55
+ printf '[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)\n' | miniminds-datetime-span
56
+ ```
57
+
58
+ Each non-empty, non-comment line (`# ...`) is parsed and canonicalized.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/minimindslab_datetime_span/__init__.py
5
+ src/minimindslab_datetime_span/__main__.py
6
+ src/minimindslab_datetime_span/core.py
7
+ src/minimindslab_datetime_span.egg-info/PKG-INFO
8
+ src/minimindslab_datetime_span.egg-info/SOURCES.txt
9
+ src/minimindslab_datetime_span.egg-info/dependency_links.txt
10
+ src/minimindslab_datetime_span.egg-info/entry_points.txt
11
+ src/minimindslab_datetime_span.egg-info/top_level.txt
12
+ tests/test_datetime_span.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ miniminds-datetime-span = minimindslab_datetime_span.__main__:main
@@ -0,0 +1,71 @@
1
+ import io
2
+ import os
3
+ import tempfile
4
+ import unittest
5
+ from contextlib import redirect_stderr, redirect_stdout
6
+
7
+ from minimindslab_datetime_span import DateTimeSpan, SpanParseError, parse_span
8
+ from minimindslab_datetime_span.__main__ import main
9
+
10
+
11
+ class DateTimeSpanTests(unittest.TestCase):
12
+ def test_acceptance_parse_and_canonical(self):
13
+ span = parse_span("[2024-01-01T00:00:00Z,2024-01-01T01:02:03+00:00)")
14
+ self.assertEqual(span.canonical(), "[2024-01-01T00:00:00Z,2024-01-01T01:02:03Z)")
15
+
16
+ def test_acceptance_end_precedes_start(self):
17
+ with self.assertRaisesRegex(SpanParseError, "end precedes start"):
18
+ parse_span("[2024-01-01T02:00:00Z,2024-01-01T01:00:00Z)")
19
+
20
+ def test_acceptance_comparison(self):
21
+ a = parse_span("[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)")
22
+ b = parse_span("[2024-01-01T00:30:00Z,2024-01-01T02:00:00Z)")
23
+ c = parse_span("[2024-01-01T01:00:00Z,2024-01-01T02:00:00Z)")
24
+ d = parse_span("[2024-01-01T00:10:00Z,2024-01-01T00:20:00Z)")
25
+
26
+ self.assertTrue(a.overlaps(b))
27
+ self.assertTrue(a.contains(d))
28
+ self.assertTrue(a.is_adjacent(c))
29
+ self.assertFalse(a.overlaps(c))
30
+
31
+ def test_cli_stdin(self):
32
+ in_data = "# comment\n\n[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)\n"
33
+ out = io.StringIO()
34
+ err = io.StringIO()
35
+ with redirect_stdout(out), redirect_stderr(err):
36
+ import sys
37
+ old_stdin = sys.stdin
38
+ sys.stdin = io.StringIO(in_data)
39
+ try:
40
+ rc = main([])
41
+ finally:
42
+ sys.stdin = old_stdin
43
+ self.assertEqual(rc, 0)
44
+ self.assertEqual(out.getvalue().strip(), "[2024-01-01T00:00:00Z,2024-01-01T01:00:00Z)")
45
+ self.assertEqual(err.getvalue(), "")
46
+
47
+ def test_cli_file(self):
48
+ fd, path = tempfile.mkstemp(text=True)
49
+ os.close(fd)
50
+ try:
51
+ with open(path, "w", encoding="utf-8") as f:
52
+ f.write("[2024-01-01T00:00:00Z,2024-01-01T00:00:01Z]\n")
53
+ out = io.StringIO()
54
+ with redirect_stdout(out):
55
+ rc = main(["--file", path])
56
+ self.assertEqual(rc, 0)
57
+ self.assertEqual(out.getvalue().strip(), "[2024-01-01T00:00:00Z,2024-01-01T00:00:01Z]")
58
+ finally:
59
+ os.unlink(path)
60
+
61
+ def test_negative_invalid_syntax(self):
62
+ with self.assertRaises(SpanParseError):
63
+ parse_span("2024-01-01T00:00:00Z,2024-01-01T01:00:00Z")
64
+
65
+ def test_negative_non_utc_rejected(self):
66
+ with self.assertRaisesRegex(SpanParseError, "UTC"):
67
+ parse_span("[2024-01-01T00:00:00+01:00,2024-01-01T01:00:00+01:00)")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()