seecr-stdlib 0.6.4__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.
- seecr_stdlib/.gitignore +1 -0
- seecr_stdlib/__init__.py +25 -0
- seecr_stdlib/_version.py +24 -0
- seecr_stdlib/config.py +78 -0
- seecr_stdlib/config_test.py +120 -0
- seecr_stdlib/dict.py +63 -0
- seecr_stdlib/dict_test.py +101 -0
- seecr_stdlib/first.py +25 -0
- seecr_stdlib/first_test.py +29 -0
- seecr_stdlib/format/__init__.py +24 -0
- seecr_stdlib/format/date.py +155 -0
- seecr_stdlib/format/date_test.py +147 -0
- seecr_stdlib/time.py +73 -0
- seecr_stdlib/time_test.py +84 -0
- seecr_stdlib-0.6.4.dist-info/METADATA +132 -0
- seecr_stdlib-0.6.4.dist-info/RECORD +19 -0
- seecr_stdlib-0.6.4.dist-info/WHEEL +5 -0
- seecr_stdlib-0.6.4.dist-info/licenses/LICENSE +674 -0
- seecr_stdlib-0.6.4.dist-info/top_level.txt +1 -0
seecr_stdlib/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
_version.py
|
seecr_stdlib/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
from .dict import Dict
|
|
25
|
+
from .first import first
|
seecr_stdlib/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.6.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 4)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'gc79af26a3'
|
seecr_stdlib/config.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import pathlib
|
|
26
|
+
import warnings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def expand_dict(obj, context):
|
|
30
|
+
if isinstance(obj, dict):
|
|
31
|
+
return {k: expand_dict(v, context) for k, v in obj.items()}
|
|
32
|
+
if isinstance(obj, list):
|
|
33
|
+
return [expand_dict(v, context) for v in obj]
|
|
34
|
+
if isinstance(obj, str):
|
|
35
|
+
return obj.format_map(context)
|
|
36
|
+
return obj # ints, None, etc. unchanged
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Config:
|
|
40
|
+
@classmethod
|
|
41
|
+
def read(cls, path, **kwargs):
|
|
42
|
+
return cls(json.loads(path.read_text()), **kwargs)
|
|
43
|
+
|
|
44
|
+
def __init__(self, config_file, required=None):
|
|
45
|
+
if isinstance(config_file, pathlib.Path):
|
|
46
|
+
warnings.warn(
|
|
47
|
+
"Specifying path in __init__ is deprecated, use Config.read(path, ..) instead",
|
|
48
|
+
DeprecationWarning,
|
|
49
|
+
stacklevel=2,
|
|
50
|
+
)
|
|
51
|
+
config = json.loads(config_file.read_text())
|
|
52
|
+
elif isinstance(config_file, dict):
|
|
53
|
+
config = config_file
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError("config_file should be either a Path or dict")
|
|
56
|
+
|
|
57
|
+
self._config = config
|
|
58
|
+
if required is not None:
|
|
59
|
+
for key in required:
|
|
60
|
+
if self.get(key) is None:
|
|
61
|
+
raise RuntimeError(f"Mandatory key '{key}' not found in config")
|
|
62
|
+
|
|
63
|
+
def get(self, key, default=None):
|
|
64
|
+
value = self._config
|
|
65
|
+
for k in key.split("."):
|
|
66
|
+
if k.isdigit() and isinstance(value, list):
|
|
67
|
+
try:
|
|
68
|
+
value = value[int(k)]
|
|
69
|
+
except IndexError:
|
|
70
|
+
return default
|
|
71
|
+
else:
|
|
72
|
+
value = value.get(k)
|
|
73
|
+
if value is None:
|
|
74
|
+
return default
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
def expand_with(self, values_dict):
|
|
78
|
+
return Config(expand_dict(self._config, values_dict))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
import json
|
|
26
|
+
|
|
27
|
+
from .config import Config
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_config_get():
|
|
31
|
+
config = {
|
|
32
|
+
"root": {
|
|
33
|
+
"text": "The text",
|
|
34
|
+
"number": 42,
|
|
35
|
+
"boolean": True,
|
|
36
|
+
"values": [1, 2, 3, 4, 5],
|
|
37
|
+
"sub": {
|
|
38
|
+
"boolean": False,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"complex": {"values": [{"1": ["one"]}, {"2": "two"}]},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
config = Config(config)
|
|
45
|
+
assert config.get("root.text") == "The text"
|
|
46
|
+
assert config.get("root.texts") is None
|
|
47
|
+
assert config.get("root.texts", default="X") == "X"
|
|
48
|
+
assert config.get("root.number") == 42
|
|
49
|
+
assert config.get("root.boolean") is True
|
|
50
|
+
assert config.get("root.sub.boolean") is False
|
|
51
|
+
assert config.get("root.values") == [1, 2, 3, 4, 5]
|
|
52
|
+
assert config.get("root.values.0") == 1
|
|
53
|
+
assert config.get("root.values.0") == 1
|
|
54
|
+
assert config.get("root.values.5") is None
|
|
55
|
+
assert config.get("root.values.5", default="X") == "X"
|
|
56
|
+
assert config.get("values.sub") is None
|
|
57
|
+
assert config.get("values.sub", default=1) == 1
|
|
58
|
+
|
|
59
|
+
assert config.get("complex.values.0.1.0") == "one"
|
|
60
|
+
assert config.get("complex.values.1.2") == "two"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_config_required(tmp_path):
|
|
64
|
+
with pytest.raises(RuntimeError) as e:
|
|
65
|
+
Config({}, required=["root"])
|
|
66
|
+
|
|
67
|
+
assert str(e.value) == "Mandatory key 'root' not found in config"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_config_arg(tmp_path):
|
|
71
|
+
(config_file := tmp_path / "config.json").write_text(json.dumps({"key": "value"}))
|
|
72
|
+
with pytest.warns(DeprecationWarning) as warnings:
|
|
73
|
+
assert Config(config_file=config_file).get("key") == "value"
|
|
74
|
+
|
|
75
|
+
assert len(warnings) == 1
|
|
76
|
+
assert (
|
|
77
|
+
"Specifying path in __init__ is deprecated, use Config.read(path, ..) instead"
|
|
78
|
+
in str(warnings[0].message)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert Config({"key": "value"}).get("key") == "value"
|
|
82
|
+
|
|
83
|
+
with pytest.raises(ValueError) as e:
|
|
84
|
+
Config([])
|
|
85
|
+
|
|
86
|
+
assert str(e.value) == "config_file should be either a Path or dict"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_config_expand():
|
|
90
|
+
config = Config(
|
|
91
|
+
{
|
|
92
|
+
"root": {
|
|
93
|
+
"text": "The text {name}",
|
|
94
|
+
},
|
|
95
|
+
"deeper": {"levels": {"work": {"too": "The answer is {answer}"}}},
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expanded_config = config.expand_with(dict(name="has no name", answer=42))
|
|
100
|
+
|
|
101
|
+
assert config.get("root.text") == "The text {name}"
|
|
102
|
+
assert expanded_config.get("root.text") == "The text has no name"
|
|
103
|
+
|
|
104
|
+
assert config.get("deeper.levels.work.too") == "The answer is {answer}"
|
|
105
|
+
assert expanded_config.get("deeper.levels.work.too") == "The answer is 42"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_my_config(tmp_path):
|
|
109
|
+
(config_file := tmp_path / "config.json").write_text(
|
|
110
|
+
json.dumps({"key": "value", "nested": {"key": "nested_value"}})
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
class MyConfig(Config):
|
|
114
|
+
def __init__(self, config):
|
|
115
|
+
super().__init__(config, required=["key", "nested.key"])
|
|
116
|
+
|
|
117
|
+
config = MyConfig.read(config_file)
|
|
118
|
+
|
|
119
|
+
assert config.get("key") == "value"
|
|
120
|
+
assert config.get("nested.key") == "nested_value"
|
seecr_stdlib/dict.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
class Dict(dict):
|
|
25
|
+
"""A dict whose keys can also be read and written as attributes:
|
|
26
|
+
d.answer is d["answer"].
|
|
27
|
+
|
|
28
|
+
Attributes of the class itself (methods like get, items, keys, update,
|
|
29
|
+
and dunder names) always win, so a key can never hide a method:
|
|
30
|
+
d["items"] = 1 leaves d.items as the method; use d["items"] for the value.
|
|
31
|
+
Assigning such a name as an attribute (d.items = 1) raises AttributeError
|
|
32
|
+
instead of silently storing a key that could not be read back as d.items.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__slots__ = () # no instance __dict__: every attribute write goes to the keys
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, key):
|
|
38
|
+
# Only called when normal lookup fails, i.e. never for methods.
|
|
39
|
+
try:
|
|
40
|
+
return self[key]
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise AttributeError(
|
|
43
|
+
f"{type(self).__name__!r} object has no attribute or key {key!r}"
|
|
44
|
+
) from None
|
|
45
|
+
|
|
46
|
+
def __setattr__(self, key, value):
|
|
47
|
+
if hasattr(type(self), key):
|
|
48
|
+
raise AttributeError(
|
|
49
|
+
f"{key!r} is a {type(self).__name__} attribute; use d[{key!r}] = ... instead"
|
|
50
|
+
)
|
|
51
|
+
self[key] = value
|
|
52
|
+
|
|
53
|
+
def __delattr__(self, key):
|
|
54
|
+
if hasattr(type(self), key):
|
|
55
|
+
raise AttributeError(f"cannot delete {type(self).__name__} attribute {key!r}")
|
|
56
|
+
try:
|
|
57
|
+
del self[key]
|
|
58
|
+
except KeyError:
|
|
59
|
+
raise AttributeError(key) from None
|
|
60
|
+
|
|
61
|
+
def __dir__(self):
|
|
62
|
+
# Include string keys that are valid identifiers, for tab completion.
|
|
63
|
+
return sorted(set(super().__dir__()) | {k for k in self if isinstance(k, str) and k.isidentifier()})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
import json
|
|
26
|
+
import pickle
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from .dict import Dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_dict():
|
|
34
|
+
d = Dict()
|
|
35
|
+
d["answer"] = 42
|
|
36
|
+
assert d["answer"] == 42
|
|
37
|
+
assert d.answer == 42
|
|
38
|
+
|
|
39
|
+
d = Dict()
|
|
40
|
+
d.answer = 42
|
|
41
|
+
|
|
42
|
+
assert d.answer == 42
|
|
43
|
+
assert d["answer"] == 42
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_keys_do_not_override_methods():
|
|
47
|
+
d = Dict(items=1, get=2, keys=3, update=4)
|
|
48
|
+
assert callable(d.items) and callable(d.get) and callable(d.keys)
|
|
49
|
+
assert d["items"] == 1 and d.get("get") == 2
|
|
50
|
+
assert sorted(d.keys()) == ["get", "items", "keys", "update"]
|
|
51
|
+
d.update(answer=42)
|
|
52
|
+
assert d.answer == 42
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_setting_method_name_as_attribute_is_refused():
|
|
56
|
+
d = Dict()
|
|
57
|
+
with pytest.raises(AttributeError):
|
|
58
|
+
d.items = 1
|
|
59
|
+
with pytest.raises(AttributeError):
|
|
60
|
+
d.__class__ = dict
|
|
61
|
+
assert d == {}
|
|
62
|
+
d["items"] = 1 # item syntax still works
|
|
63
|
+
assert d["items"] == 1 and callable(d.items)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_missing_attribute():
|
|
67
|
+
d = Dict(a=1)
|
|
68
|
+
with pytest.raises(AttributeError):
|
|
69
|
+
d.missing
|
|
70
|
+
assert getattr(d, "missing", None) is None
|
|
71
|
+
assert hasattr(d, "a") and not hasattr(d, "missing")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_delete_attribute():
|
|
75
|
+
d = Dict(a=1)
|
|
76
|
+
del d.a
|
|
77
|
+
assert d == {}
|
|
78
|
+
with pytest.raises(AttributeError):
|
|
79
|
+
del d.a
|
|
80
|
+
with pytest.raises(AttributeError):
|
|
81
|
+
del d.items
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_no_instance_dict():
|
|
85
|
+
d = Dict()
|
|
86
|
+
d.answer = 42
|
|
87
|
+
assert d == {"answer": 42}
|
|
88
|
+
assert not hasattr(d, "__dict__")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_dir_includes_keys():
|
|
92
|
+
d = Dict(Name="x", **{"not valid": 1})
|
|
93
|
+
assert "Name" in dir(d) and "items" in dir(d) and "not valid" not in dir(d)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_still_a_dict():
|
|
97
|
+
d = Dict(a=1, b=Dict(c=2))
|
|
98
|
+
assert isinstance(d, dict)
|
|
99
|
+
assert json.loads(json.dumps(d)) == {"a": 1, "b": {"c": 2}}
|
|
100
|
+
for clone in (copy.copy(d), copy.deepcopy(d), pickle.loads(pickle.dumps(d))):
|
|
101
|
+
assert clone == d and type(clone) is Dict and clone.b.c == 2
|
seecr_stdlib/first.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
def first(collection):
|
|
25
|
+
return collection[0] if len(collection) > 0 else None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
from .first import first
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_first():
|
|
28
|
+
assert first([]) is None
|
|
29
|
+
assert first([1]) == 1
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
from .date import *
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
## begin license ##
|
|
2
|
+
#
|
|
3
|
+
# Seecr StdLib provides convenient tools for Seecr developers
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2025-2026 Seecr (Seek You Too B.V.) https://seecr.nl
|
|
6
|
+
#
|
|
7
|
+
# This file is part of "Seecr Standard Library"
|
|
8
|
+
#
|
|
9
|
+
# "Seecr Standard Library" is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# "Seecr Standard Library" is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with "Seecr Standard Library". If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
## end license ##
|
|
23
|
+
|
|
24
|
+
__all__ = ["format_date"]
|
|
25
|
+
|
|
26
|
+
import zoneinfo
|
|
27
|
+
from functools import partial
|
|
28
|
+
|
|
29
|
+
from seecr_stdlib.time import float_as_datetime
|
|
30
|
+
from dateutil import parser
|
|
31
|
+
import datetime
|
|
32
|
+
|
|
33
|
+
NL_TZ = zoneinfo.ZoneInfo("Europe/Amsterdam")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _in_tz(date, tzinfo):
|
|
37
|
+
# Naive datetimes are taken to be in tzinfo already; astimezone() would
|
|
38
|
+
# assume the server's local time and make the output machine dependent.
|
|
39
|
+
if date.tzinfo is None:
|
|
40
|
+
return date.replace(tzinfo=tzinfo)
|
|
41
|
+
return date.astimezone(tzinfo)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _iso_style(date):
|
|
45
|
+
return date.isoformat().split("T")[0]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _zulu_style(date):
|
|
49
|
+
return date.isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _nl_style(date, time=False, tzinfo=NL_TZ):
|
|
53
|
+
if isinstance(date, datetime.datetime) and time:
|
|
54
|
+
date = _in_tz(date, tzinfo)
|
|
55
|
+
|
|
56
|
+
fmt = "%d-%m-%Y"
|
|
57
|
+
if time:
|
|
58
|
+
fmt += " %H:%M"
|
|
59
|
+
return date.strftime(fmt)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
days_nl = list(
|
|
63
|
+
zip(
|
|
64
|
+
(
|
|
65
|
+
"maandag",
|
|
66
|
+
"dinsdag",
|
|
67
|
+
"woensdag",
|
|
68
|
+
"donderdag",
|
|
69
|
+
"vrijdag",
|
|
70
|
+
"zaterdag",
|
|
71
|
+
"zondag",
|
|
72
|
+
),
|
|
73
|
+
("ma", "di", "wo", "do", "vr", "za", "zo"),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
months_nl = list(
|
|
77
|
+
zip(
|
|
78
|
+
(
|
|
79
|
+
"januari",
|
|
80
|
+
"februari",
|
|
81
|
+
"maart",
|
|
82
|
+
"april",
|
|
83
|
+
"mei",
|
|
84
|
+
"juni",
|
|
85
|
+
"juli",
|
|
86
|
+
"augustus",
|
|
87
|
+
"september",
|
|
88
|
+
"oktober",
|
|
89
|
+
"november",
|
|
90
|
+
"december",
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"jan",
|
|
94
|
+
"feb",
|
|
95
|
+
"mrt",
|
|
96
|
+
"apr",
|
|
97
|
+
"mei",
|
|
98
|
+
"jun",
|
|
99
|
+
"jul",
|
|
100
|
+
"aug",
|
|
101
|
+
"sep",
|
|
102
|
+
"okt",
|
|
103
|
+
"nov",
|
|
104
|
+
"dec",
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _nllang_style(date, pos, time=False, tzinfo=NL_TZ):
|
|
111
|
+
if isinstance(date, datetime.datetime) and time:
|
|
112
|
+
date = _in_tz(date, tzinfo)
|
|
113
|
+
|
|
114
|
+
day_name = days_nl[date.weekday()][pos]
|
|
115
|
+
month_name = months_nl[date.month - 1][pos]
|
|
116
|
+
result = f"{day_name} {date.day} {month_name} {date.year}"
|
|
117
|
+
if time:
|
|
118
|
+
result += f" {date.hour:02}:{date.minute:02}"
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
styles = {
|
|
123
|
+
"nl": _nl_style,
|
|
124
|
+
"nl_tijd": partial(_nl_style, time=True),
|
|
125
|
+
"nllang": partial(_nllang_style, pos=0),
|
|
126
|
+
"nlkort": partial(_nllang_style, pos=1),
|
|
127
|
+
"nllang_tijd": partial(_nllang_style, pos=0, time=True),
|
|
128
|
+
"nlkort_tijd": partial(_nllang_style, pos=1, time=True),
|
|
129
|
+
"zulu": _zulu_style,
|
|
130
|
+
}
|
|
131
|
+
styles["nl-t"] = styles["nl_tijd"]
|
|
132
|
+
styles["nl-l"] = styles["nllang"]
|
|
133
|
+
styles["nl-k"] = styles["nlkort"]
|
|
134
|
+
styles["nl-lt"] = styles["nllang_tijd"]
|
|
135
|
+
styles["nl-kt"] = styles["nlkort_tijd"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def format_date(date, style="iso"):
|
|
139
|
+
"""Uniformly format a date according to the given style.
|
|
140
|
+
|
|
141
|
+
To use as jinja filter, add 'format_date' to the filters dict in your jinja environment.
|
|
142
|
+
Usage:
|
|
143
|
+
{{ date | format_date('nl') }}
|
|
144
|
+
"""
|
|
145
|
+
if date is None:
|
|
146
|
+
return ""
|
|
147
|
+
if isinstance(date, str):
|
|
148
|
+
try:
|
|
149
|
+
date = parser.isoparse(date)
|
|
150
|
+
except ValueError:
|
|
151
|
+
return date
|
|
152
|
+
if isinstance(date, (int, float)):
|
|
153
|
+
date = float_as_datetime(date)
|
|
154
|
+
fn = styles.get(style, _iso_style)
|
|
155
|
+
return fn(date)
|