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.
@@ -0,0 +1 @@
1
+ _version.py
@@ -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
@@ -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)