sysnet-pyutils 1.7.4__tar.gz → 1.7.5__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.
Files changed (59) hide show
  1. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/PKG-INFO +25 -1
  2. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/README.md +24 -0
  3. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/pyproject.toml +1 -1
  4. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/__init__.py +2 -0
  5. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/data_utils.py +44 -0
  6. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/PKG-INFO +25 -1
  7. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/SOURCES.txt +1 -0
  8. sysnet_pyutils-1.7.5/tests/test_data_utils_datetime.py +156 -0
  9. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/LICENSE +0 -0
  10. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/setup.cfg +0 -0
  11. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/barcode.py +0 -0
  12. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/constants.py +0 -0
  13. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/cron_utils/__init__.py +0 -0
  14. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/cron_utils/validation.py +0 -0
  15. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/domino.py +0 -0
  16. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/geo.py +0 -0
  17. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/globalmodel/__init__.py +0 -0
  18. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/globalmodel/global_model.py +0 -0
  19. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/globalmodel/models.py +0 -0
  20. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/globalmodel/timeutils.py +0 -0
  21. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/ident.py +0 -0
  22. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/log.py +0 -0
  23. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/models/__init__.py +0 -0
  24. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/models/general.py +0 -0
  25. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/ses.py +0 -0
  26. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/syslog/__init__.py +0 -0
  27. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/syslog/factory.py +0 -0
  28. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/tools.py +0 -0
  29. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils/utils.py +0 -0
  30. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/dependency_links.txt +0 -0
  31. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/not-zip-safe +0 -0
  32. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/requires.txt +0 -0
  33. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/sysnet_pyutils.egg-info/top_level.txt +0 -0
  34. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_cron_utils.py +0 -0
  35. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_cron_validation_edge.py +0 -0
  36. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_data_utils.py +0 -0
  37. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_data_utils_edge.py +0 -0
  38. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_data_utils_missing_branches.py +0 -0
  39. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_domino.py +0 -0
  40. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_domino_edge.py +0 -0
  41. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_geo.py +0 -0
  42. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_global_model_recursive.py +0 -0
  43. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_global_model_timezone.py +0 -0
  44. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_ident_barcode.py +0 -0
  45. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_log_importerror_branch.py +0 -0
  46. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_log_mongo.py +0 -0
  47. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_models_contact.py +0 -0
  48. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_models_general.py +0 -0
  49. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_models_general_edge.py +0 -0
  50. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_package_init_version.py +0 -0
  51. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_recursive_update.py +0 -0
  52. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_ses.py +0 -0
  53. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_ses_importerror_branch.py +0 -0
  54. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_syslog_factory.py +0 -0
  55. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_tools.py +0 -0
  56. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_utils.py +0 -0
  57. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_utils_edge.py +0 -0
  58. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_utils_log_base64.py +0 -0
  59. {sysnet_pyutils-1.7.4 → sysnet_pyutils-1.7.5}/tests/test_utils_missing_branches.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-pyutils
3
- Version: 1.7.4
3
+ Version: 1.7.5
4
4
  Summary: SYSNET Python Utilities - kompletní sada nástrojů pro Python aplikace
5
5
  Author-email: Data Developer <info@sysnet.cz>
6
6
  Maintainer-email: SYSNET Development Team <info@sysnet.cz>
@@ -958,6 +958,7 @@ from sysnet_pyutils import (
958
958
  get_dict_value_email, get_dict_value_code, get_dict_value_uuid,
959
959
  convert_iso_to_date, convert_cz_to_date,
960
960
  paging_to_mongo, address_to_string,
961
+ normalize_datetime, localize_datetime,
961
962
  )
962
963
 
963
964
  data = {'name': 'John', 'age': '30', 'email': 'john@example.com'}
@@ -977,6 +978,21 @@ paging = paging_to_mongo(start=20, page_size=10)
977
978
  # Adresa na string
978
979
  address = {'street': 'Main St 123', 'city': 'Prague', 'zip': '110 00'}
979
980
  addr_str = address_to_string(address)
981
+
982
+ # Normalizace datetime na UTC
983
+ # Naivní datetime dostane zónu dle env TZ (výchozí: Europe/Prague), pak se převede na UTC.
984
+ # Čistý date → půlnoc v lokální zóně → UTC.
985
+ from datetime import date, datetime
986
+ utc_dt = normalize_datetime(datetime(2024, 7, 1, 12, 0, 0)) # naive → Prague → UTC
987
+ utc_dt = normalize_datetime(date(2024, 7, 1)) # půlnoc Prague → UTC
988
+ utc_dt = normalize_datetime(None) # None → None
989
+
990
+ # Lokalizace datetime do lokální zóny (env TZ)
991
+ # Naivní datetime se považuje za UTC a převede do lokální zóny.
992
+ # Čistý date → půlnoc UTC → lokální zóna.
993
+ local_dt = localize_datetime(datetime(2024, 7, 1, 10, 0, 0)) # naive UTC → Prague
994
+ local_dt = localize_datetime(date(2024, 7, 1)) # půlnoc UTC → Prague
995
+ local_dt = localize_datetime(None) # None → None
980
996
  ```
981
997
 
982
998
  ---
@@ -1299,6 +1315,14 @@ pyutils/
1299
1315
 
1300
1316
  ## 📚 Changelog
1301
1317
 
1318
+ ### Verze 1.7.5 (2026-04-07)
1319
+
1320
+ **Nové funkce:**
1321
+ - ✅ `data_utils.py` — přidána `normalize_datetime(date | datetime | None) -> datetime | None`: naivní vstup dostane zónu dle `TZ`, výstup vždy UTC
1322
+ - ✅ `data_utils.py` — přidána `localize_datetime(date | datetime | None) -> datetime | None`: naivní vstup se považuje za UTC, výstup v lokální zóně dle `TZ`
1323
+ - ✅ Obě funkce exportovány z `sysnet_pyutils/__init__.py`
1324
+ - ✅ 22 nových testů v `tests/test_data_utils_datetime.py` (včetně DST hraničních případů a roundtrip testů)
1325
+
1302
1326
  ### Verze 1.7.1 (2026-03-02)
1303
1327
 
1304
1328
  **Opravy:**
@@ -235,6 +235,7 @@ from sysnet_pyutils import (
235
235
  get_dict_value_email, get_dict_value_code, get_dict_value_uuid,
236
236
  convert_iso_to_date, convert_cz_to_date,
237
237
  paging_to_mongo, address_to_string,
238
+ normalize_datetime, localize_datetime,
238
239
  )
239
240
 
240
241
  data = {'name': 'John', 'age': '30', 'email': 'john@example.com'}
@@ -254,6 +255,21 @@ paging = paging_to_mongo(start=20, page_size=10)
254
255
  # Adresa na string
255
256
  address = {'street': 'Main St 123', 'city': 'Prague', 'zip': '110 00'}
256
257
  addr_str = address_to_string(address)
258
+
259
+ # Normalizace datetime na UTC
260
+ # Naivní datetime dostane zónu dle env TZ (výchozí: Europe/Prague), pak se převede na UTC.
261
+ # Čistý date → půlnoc v lokální zóně → UTC.
262
+ from datetime import date, datetime
263
+ utc_dt = normalize_datetime(datetime(2024, 7, 1, 12, 0, 0)) # naive → Prague → UTC
264
+ utc_dt = normalize_datetime(date(2024, 7, 1)) # půlnoc Prague → UTC
265
+ utc_dt = normalize_datetime(None) # None → None
266
+
267
+ # Lokalizace datetime do lokální zóny (env TZ)
268
+ # Naivní datetime se považuje za UTC a převede do lokální zóny.
269
+ # Čistý date → půlnoc UTC → lokální zóna.
270
+ local_dt = localize_datetime(datetime(2024, 7, 1, 10, 0, 0)) # naive UTC → Prague
271
+ local_dt = localize_datetime(date(2024, 7, 1)) # půlnoc UTC → Prague
272
+ local_dt = localize_datetime(None) # None → None
257
273
  ```
258
274
 
259
275
  ---
@@ -576,6 +592,14 @@ pyutils/
576
592
 
577
593
  ## 📚 Changelog
578
594
 
595
+ ### Verze 1.7.5 (2026-04-07)
596
+
597
+ **Nové funkce:**
598
+ - ✅ `data_utils.py` — přidána `normalize_datetime(date | datetime | None) -> datetime | None`: naivní vstup dostane zónu dle `TZ`, výstup vždy UTC
599
+ - ✅ `data_utils.py` — přidána `localize_datetime(date | datetime | None) -> datetime | None`: naivní vstup se považuje za UTC, výstup v lokální zóně dle `TZ`
600
+ - ✅ Obě funkce exportovány z `sysnet_pyutils/__init__.py`
601
+ - ✅ 22 nových testů v `tests/test_data_utils_datetime.py` (včetně DST hraničních případů a roundtrip testů)
602
+
579
603
  ### Verze 1.7.1 (2026-03-02)
580
604
 
581
605
  **Opravy:**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sysnet-pyutils"
7
- version = "1.7.4"
7
+ version = "1.7.5"
8
8
  description = "SYSNET Python Utilities - kompletní sada nástrojů pro Python aplikace"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -62,6 +62,8 @@ from sysnet_pyutils.data_utils import (
62
62
  get_dict_value_string,
63
63
  get_dict_value_uuid,
64
64
  get_object_attr,
65
+ localize_datetime,
66
+ normalize_datetime,
65
67
  paging_to_mongo,
66
68
  recursive_update,
67
69
  )
@@ -333,6 +333,50 @@ def adjust_datetime_to_utc(date_value: date | datetime | None, for_store: bool =
333
333
  return date_value
334
334
 
335
335
 
336
+ def normalize_datetime(value: date | datetime | None) -> datetime | None:
337
+ """
338
+ Převede date nebo datetime na datetime v UTC.
339
+
340
+ Naivní datetime (bez tzinfo) dostane časovou zónu dle env proměnné TZ (výchozí: Europe/Prague).
341
+ Čistý date se převede na půlnoc v lokální zóně (TZ), poté na UTC.
342
+ Hodnota None je vrácena beze změny.
343
+
344
+ :param value: Vstupní datum nebo datetime
345
+ :return: datetime v UTC, nebo None
346
+ """
347
+ if value is None:
348
+ return None
349
+ local_tz = pytz.timezone(TZ)
350
+ if isinstance(value, datetime):
351
+ if value.tzinfo is None:
352
+ value = local_tz.localize(value)
353
+ return value.astimezone(pytz.UTC)
354
+ # čistý date → půlnoc v lokální zóně → UTC
355
+ return local_tz.localize(datetime(value.year, value.month, value.day, 0, 0, 0)).astimezone(pytz.UTC)
356
+
357
+
358
+ def localize_datetime(value: date | datetime | None) -> datetime | None:
359
+ """
360
+ Převede date nebo datetime na datetime v lokální časové zóně (dle env proměnné TZ).
361
+
362
+ Naivní datetime (bez tzinfo) je považován za UTC.
363
+ Čistý date se převede na půlnoc v UTC, poté do lokální zóny.
364
+ Hodnota None je vrácena beze změny.
365
+
366
+ :param value: Vstupní datum nebo datetime
367
+ :return: datetime v lokální zóně (TZ), nebo None
368
+ """
369
+ if value is None:
370
+ return None
371
+ local_tz = pytz.timezone(TZ)
372
+ if isinstance(value, datetime):
373
+ if value.tzinfo is None:
374
+ value = pytz.UTC.localize(value)
375
+ return value.astimezone(local_tz)
376
+ # čistý date → půlnoc v UTC → lokální zóna
377
+ return pytz.UTC.localize(datetime(value.year, value.month, value.day, 0, 0, 0)).astimezone(local_tz)
378
+
379
+
336
380
  def dict_convert_date_to_str(data: dict | list) -> None:
337
381
  """
338
382
  Rekurzivně převede všechny časové položky ve slovníku na ISO text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-pyutils
3
- Version: 1.7.4
3
+ Version: 1.7.5
4
4
  Summary: SYSNET Python Utilities - kompletní sada nástrojů pro Python aplikace
5
5
  Author-email: Data Developer <info@sysnet.cz>
6
6
  Maintainer-email: SYSNET Development Team <info@sysnet.cz>
@@ -958,6 +958,7 @@ from sysnet_pyutils import (
958
958
  get_dict_value_email, get_dict_value_code, get_dict_value_uuid,
959
959
  convert_iso_to_date, convert_cz_to_date,
960
960
  paging_to_mongo, address_to_string,
961
+ normalize_datetime, localize_datetime,
961
962
  )
962
963
 
963
964
  data = {'name': 'John', 'age': '30', 'email': 'john@example.com'}
@@ -977,6 +978,21 @@ paging = paging_to_mongo(start=20, page_size=10)
977
978
  # Adresa na string
978
979
  address = {'street': 'Main St 123', 'city': 'Prague', 'zip': '110 00'}
979
980
  addr_str = address_to_string(address)
981
+
982
+ # Normalizace datetime na UTC
983
+ # Naivní datetime dostane zónu dle env TZ (výchozí: Europe/Prague), pak se převede na UTC.
984
+ # Čistý date → půlnoc v lokální zóně → UTC.
985
+ from datetime import date, datetime
986
+ utc_dt = normalize_datetime(datetime(2024, 7, 1, 12, 0, 0)) # naive → Prague → UTC
987
+ utc_dt = normalize_datetime(date(2024, 7, 1)) # půlnoc Prague → UTC
988
+ utc_dt = normalize_datetime(None) # None → None
989
+
990
+ # Lokalizace datetime do lokální zóny (env TZ)
991
+ # Naivní datetime se považuje za UTC a převede do lokální zóny.
992
+ # Čistý date → půlnoc UTC → lokální zóna.
993
+ local_dt = localize_datetime(datetime(2024, 7, 1, 10, 0, 0)) # naive UTC → Prague
994
+ local_dt = localize_datetime(date(2024, 7, 1)) # půlnoc UTC → Prague
995
+ local_dt = localize_datetime(None) # None → None
980
996
  ```
981
997
 
982
998
  ---
@@ -1299,6 +1315,14 @@ pyutils/
1299
1315
 
1300
1316
  ## 📚 Changelog
1301
1317
 
1318
+ ### Verze 1.7.5 (2026-04-07)
1319
+
1320
+ **Nové funkce:**
1321
+ - ✅ `data_utils.py` — přidána `normalize_datetime(date | datetime | None) -> datetime | None`: naivní vstup dostane zónu dle `TZ`, výstup vždy UTC
1322
+ - ✅ `data_utils.py` — přidána `localize_datetime(date | datetime | None) -> datetime | None`: naivní vstup se považuje za UTC, výstup v lokální zóně dle `TZ`
1323
+ - ✅ Obě funkce exportovány z `sysnet_pyutils/__init__.py`
1324
+ - ✅ 22 nových testů v `tests/test_data_utils_datetime.py` (včetně DST hraničních případů a roundtrip testů)
1325
+
1302
1326
  ### Verze 1.7.1 (2026-03-02)
1303
1327
 
1304
1328
  **Opravy:**
@@ -31,6 +31,7 @@ sysnet_pyutils/syslog/factory.py
31
31
  tests/test_cron_utils.py
32
32
  tests/test_cron_validation_edge.py
33
33
  tests/test_data_utils.py
34
+ tests/test_data_utils_datetime.py
34
35
  tests/test_data_utils_edge.py
35
36
  tests/test_data_utils_missing_branches.py
36
37
  tests/test_domino.py
@@ -0,0 +1,156 @@
1
+ """
2
+ Testy pro normalize_datetime a localize_datetime v sysnet_pyutils.data_utils
3
+ """
4
+
5
+ from datetime import date, datetime
6
+
7
+ import pytz
8
+ import pytest
9
+
10
+ from sysnet_pyutils.data_utils import localize_datetime, normalize_datetime
11
+
12
+ # Referenční zóna shodná s výchozí TZ env proměnnou (Europe/Prague)
13
+ PRAGUE = pytz.timezone("Europe/Prague")
14
+ UTC = pytz.UTC
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # normalize_datetime
19
+ # ---------------------------------------------------------------------------
20
+
21
+ class TestNormalizeDatetime:
22
+ """normalize_datetime → výstup vždy datetime v UTC (nebo None)"""
23
+
24
+ def test_none_returns_none(self):
25
+ assert normalize_datetime(None) is None
26
+
27
+ def test_naive_datetime_treated_as_local(self):
28
+ # naivní 2024-03-10 12:00:00 → Prague je UTC+1 (zimní čas)
29
+ naive = datetime(2024, 3, 10, 12, 0, 0)
30
+ result = normalize_datetime(naive)
31
+ expected = PRAGUE.localize(naive).astimezone(UTC)
32
+ assert result == expected
33
+
34
+ def test_naive_datetime_summer_time(self):
35
+ # letní čas: Prague je UTC+2
36
+ naive = datetime(2024, 7, 1, 12, 0, 0)
37
+ result = normalize_datetime(naive)
38
+ expected = PRAGUE.localize(naive).astimezone(UTC)
39
+ assert result == expected
40
+
41
+ def test_aware_utc_unchanged(self):
42
+ aware = datetime(2024, 6, 15, 10, 0, 0, tzinfo=UTC)
43
+ result = normalize_datetime(aware)
44
+ assert result == aware
45
+ assert result.tzinfo == UTC
46
+
47
+ def test_aware_prague_converted_to_utc(self):
48
+ aware = PRAGUE.localize(datetime(2024, 6, 15, 12, 0, 0))
49
+ result = normalize_datetime(aware)
50
+ assert result.tzinfo == UTC
51
+ assert result == aware.astimezone(UTC)
52
+
53
+ def test_date_midnight_local_to_utc(self):
54
+ # date 2024-01-15 → půlnoc v Prague (UTC+1 zimní čas) → 23:00 předchozího dne UTC
55
+ d = date(2024, 1, 15)
56
+ result = normalize_datetime(d)
57
+ expected = PRAGUE.localize(datetime(2024, 1, 15, 0, 0, 0)).astimezone(UTC)
58
+ assert result == expected
59
+ assert result.tzinfo == UTC
60
+
61
+ def test_date_summer_midnight_local_to_utc(self):
62
+ # date 2024-07-01 → půlnoc v Prague (UTC+2) → 22:00 předchozího dne UTC
63
+ d = date(2024, 7, 1)
64
+ result = normalize_datetime(d)
65
+ expected = PRAGUE.localize(datetime(2024, 7, 1, 0, 0, 0)).astimezone(UTC)
66
+ assert result == expected
67
+
68
+ def test_result_is_always_datetime(self):
69
+ assert isinstance(normalize_datetime(date(2024, 1, 1)), datetime)
70
+ assert isinstance(normalize_datetime(datetime(2024, 1, 1)), datetime)
71
+
72
+ def test_result_tzinfo_is_utc(self):
73
+ result = normalize_datetime(datetime(2024, 5, 1, 8, 0, 0))
74
+ assert result.utcoffset().total_seconds() == 0
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # localize_datetime
79
+ # ---------------------------------------------------------------------------
80
+
81
+ class TestLocalizeDatetime:
82
+ """localize_datetime → výstup vždy datetime v lokální zóně (TZ) nebo None"""
83
+
84
+ def test_none_returns_none(self):
85
+ assert localize_datetime(None) is None
86
+
87
+ def test_naive_datetime_treated_as_utc(self):
88
+ # naivní → považuje se za UTC → převede do Prague
89
+ naive = datetime(2024, 3, 10, 11, 0, 0)
90
+ result = localize_datetime(naive)
91
+ expected = UTC.localize(naive).astimezone(PRAGUE)
92
+ assert result == expected
93
+
94
+ def test_aware_utc_converted_to_local(self):
95
+ aware = datetime(2024, 1, 15, 23, 0, 0, tzinfo=UTC)
96
+ result = localize_datetime(aware)
97
+ # UTC+1 zimní čas → 00:00 následujícího dne v Prague
98
+ assert result.tzinfo.zone == "Europe/Prague"
99
+ assert result == aware.astimezone(PRAGUE)
100
+
101
+ def test_aware_prague_unchanged_zone(self):
102
+ aware = PRAGUE.localize(datetime(2024, 6, 15, 12, 0, 0))
103
+ result = localize_datetime(aware)
104
+ assert result.tzinfo.zone == "Europe/Prague"
105
+
106
+ def test_date_midnight_utc_to_local(self):
107
+ # date 2024-07-01 → půlnoc UTC → 02:00 Prague (UTC+2 letní čas)
108
+ d = date(2024, 7, 1)
109
+ result = localize_datetime(d)
110
+ expected = UTC.localize(datetime(2024, 7, 1, 0, 0, 0)).astimezone(PRAGUE)
111
+ assert result == expected
112
+ assert result.tzinfo.zone == "Europe/Prague"
113
+
114
+ def test_date_winter_midnight_utc_to_local(self):
115
+ # date 2024-01-15 → půlnoc UTC → 01:00 Prague (UTC+1 zimní čas)
116
+ d = date(2024, 1, 15)
117
+ result = localize_datetime(d)
118
+ expected = UTC.localize(datetime(2024, 1, 15, 0, 0, 0)).astimezone(PRAGUE)
119
+ assert result == expected
120
+
121
+ def test_result_is_always_datetime(self):
122
+ assert isinstance(localize_datetime(date(2024, 1, 1)), datetime)
123
+ assert isinstance(localize_datetime(datetime(2024, 1, 1)), datetime)
124
+
125
+ def test_result_tzinfo_is_local(self):
126
+ result = localize_datetime(datetime(2024, 5, 1, 8, 0, 0))
127
+ assert result.tzinfo.zone == "Europe/Prague"
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Symetrie: roundtrip normalize → localize
132
+ # ---------------------------------------------------------------------------
133
+
134
+ class TestRoundtrip:
135
+ """normalize_datetime a localize_datetime jsou inverzní operace."""
136
+
137
+ @pytest.mark.parametrize("naive", [
138
+ datetime(2024, 1, 15, 10, 30, 0),
139
+ datetime(2024, 7, 1, 0, 0, 0),
140
+ datetime(2024, 3, 31, 1, 30, 0), # těsně před přechodem na letní čas
141
+ ])
142
+ def test_normalize_then_localize_roundtrip(self, naive):
143
+ utc = normalize_datetime(naive)
144
+ back = localize_datetime(utc)
145
+ # Po roundtripu musí být hodnota čas. shodná
146
+ assert back.astimezone(UTC) == utc
147
+
148
+ @pytest.mark.parametrize("d", [
149
+ date(2024, 1, 15),
150
+ date(2024, 7, 1),
151
+ ])
152
+ def test_date_normalize_then_localize(self, d):
153
+ utc = normalize_datetime(d)
154
+ local = localize_datetime(utc)
155
+ # Půlnoc v lokální zóně → UTC → zpět do lokální → stále půlnoc
156
+ assert local.hour == 0 and local.minute == 0 and local.second == 0
File without changes
File without changes