wwvb 7.0.0__tar.gz → 8.0.0rc2__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 (75) hide show
  1. wwvb-8.0.0rc2/.forgejo/workflows/ci.yml +65 -0
  2. wwvb-8.0.0rc2/.forgejo/workflows/cron.yml +37 -0
  3. {wwvb-7.0.0/.github → wwvb-8.0.0rc2/.forgejo}/workflows/release.yml +8 -13
  4. {wwvb-7.0.0 → wwvb-8.0.0rc2}/.gitignore +1 -0
  5. {wwvb-7.0.0 → wwvb-8.0.0rc2}/.pre-commit-config.yaml +8 -2
  6. {wwvb-7.0.0 → wwvb-8.0.0rc2}/Makefile +1 -1
  7. {wwvb-7.0.0/src/wwvb.egg-info → wwvb-8.0.0rc2}/PKG-INFO +5 -12
  8. {wwvb-7.0.0 → wwvb-8.0.0rc2}/README.md +1 -5
  9. {wwvb-7.0.0 → wwvb-8.0.0rc2}/pyproject.toml +3 -6
  10. {wwvb-7.0.0 → wwvb-8.0.0rc2}/requirements-dev.txt +3 -1
  11. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/__init__.py +29 -9
  12. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/__version__.py +3 -3
  13. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/decode.py +1 -1
  14. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/gen.py +6 -3
  15. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/iersdata.json +1 -1
  16. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/updateiers.py +4 -1
  17. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/wwvbtk.py +6 -4
  18. {wwvb-7.0.0 → wwvb-8.0.0rc2/src/wwvb.egg-info}/PKG-INFO +5 -12
  19. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb.egg-info/SOURCES.txt +3 -4
  20. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testcli.py +11 -3
  21. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testls.py +0 -4
  22. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testpm.py +0 -4
  23. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testuwwvb.py +2 -11
  24. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testwwvb.py +8 -8
  25. wwvb-7.0.0/.github/workflows/cron.yml +0 -48
  26. wwvb-7.0.0/.github/workflows/test.yml +0 -126
  27. wwvb-7.0.0/codecov.yml +0 -0
  28. {wwvb-7.0.0 → wwvb-8.0.0rc2}/.readthedocs.yaml +0 -0
  29. {wwvb-7.0.0 → wwvb-8.0.0rc2}/LICENSES/CC0-1.0.txt +0 -0
  30. {wwvb-7.0.0 → wwvb-8.0.0rc2}/LICENSES/GPL-3.0-only.txt +0 -0
  31. {wwvb-7.0.0 → wwvb-8.0.0rc2}/LICENSES/Unlicense.txt +0 -0
  32. {wwvb-7.0.0 → wwvb-8.0.0rc2}/doc/_static/.empty +0 -0
  33. {wwvb-7.0.0 → wwvb-8.0.0rc2}/doc/conf.py +0 -0
  34. {wwvb-7.0.0 → wwvb-8.0.0rc2}/doc/index.rst +0 -0
  35. {wwvb-7.0.0 → wwvb-8.0.0rc2}/requirements.txt +0 -0
  36. {wwvb-7.0.0 → wwvb-8.0.0rc2}/setup.cfg +0 -0
  37. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/uwwvb.py +0 -0
  38. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/dut1table.py +0 -0
  39. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/iersdata.json.license +0 -0
  40. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/iersdata.py +0 -0
  41. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/py.typed +0 -0
  42. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb/tz.py +0 -0
  43. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb.egg-info/dependency_links.txt +0 -0
  44. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb.egg-info/entry_points.txt +0 -0
  45. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb.egg-info/requires.txt +0 -0
  46. {wwvb-7.0.0 → wwvb-8.0.0rc2}/src/wwvb.egg-info/top_level.txt +0 -0
  47. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/testdaylight.py +0 -0
  48. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/1998leapsecond +0 -0
  49. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/2012leapsecond +0 -0
  50. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/all-headers +0 -0
  51. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/bar +0 -0
  52. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/both +0 -0
  53. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/cradek +0 -0
  54. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/duration +0 -0
  55. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/enddst-phase +0 -0
  56. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/enddst-phase-2 +0 -0
  57. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/endleapyear +0 -0
  58. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/leapday1 +0 -0
  59. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/leapday28 +0 -0
  60. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/leapday29 +0 -0
  61. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/negleapsecond +0 -0
  62. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/nextdst +0 -0
  63. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/nextst +0 -0
  64. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/nonleapday1 +0 -0
  65. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/nonleapday28 +0 -0
  66. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/phase +0 -0
  67. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/startdst +0 -0
  68. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/startdst-phase +0 -0
  69. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/startdst-phase-2 +0 -0
  70. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/startleapyear +0 -0
  71. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/startst +0 -0
  72. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/y2k +0 -0
  73. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/y2k-1 +0 -0
  74. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/y2k1 +0 -0
  75. {wwvb-7.0.0 → wwvb-8.0.0rc2}/test/wwvbgen_testcases/y2k1-1 +0 -0
@@ -0,0 +1,65 @@
1
+ # SPDX-FileCopyrightText: 2021-2025 Jeff Epler
2
+ #
3
+ # SPDX-License-Identifier: CC0-1.0
4
+
5
+ name: Test wwvbpy
6
+
7
+ on:
8
+ push:
9
+ pull_request:
10
+ release:
11
+ types:
12
+ - created # forgejo does not document the value "created"
13
+ - published
14
+
15
+ # (may not be used by forgejo, but is ignored for now)
16
+ concurrency:
17
+ group: ${{ forge.workflow }}-${{ forge.ref_name }}-${{ forge.event_name }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: codeberg-tiny
23
+ container:
24
+ image: docker.io/library/python:3-trixie
25
+ steps:
26
+ - name: Set up node
27
+ run: apt-get update && apt-get install -y --no-install-recommends nodejs
28
+
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Set up uv
32
+ run: pip install --break-system-packages uv
33
+
34
+ - name: Run main tests
35
+ run: |
36
+ for version in 3.10 3.11 3.12 3.13; do
37
+ echo "::group::Test with Python $version"
38
+ uv venv --python $version _venv_${version}
39
+ . _venv_${version}/bin/activate
40
+ uv pip install -r requirements-dev.txt
41
+ env PYTHONPATH=src python -mcoverage run -p -m unittest discover -s test
42
+ deactivate
43
+ echo "::endgroup::"
44
+ done
45
+
46
+ for version in 3.13; do
47
+ echo "::group::Aggregate coverage and run final checks"
48
+ . _venv_${version}/bin/activate
49
+ make -j $(nproc) -O mypy test_venv
50
+ python -mcoverage combine -q
51
+ python -mcoverage json --include "src/**/*.py"
52
+ python -mcoverage report --fail-under=100 --include "src/**/*.py"
53
+ prek --all-files
54
+ deactivate
55
+ echo "::endgroup::"
56
+ done
57
+
58
+ - name: Upload Coverage as artifact
59
+ if: always()
60
+ uses: actions/upload-artifact@v3
61
+ with:
62
+ path: coverage.json
63
+
64
+ - name: Check build directory size
65
+ run: du -shx
@@ -0,0 +1,37 @@
1
+ # SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
+ #
3
+ # SPDX-License-Identifier: CC0-1.0
4
+
5
+ name: Update DUT1 data
6
+
7
+ on:
8
+ schedule:
9
+ - cron: '0 10 2 * *'
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ update-dut1:
14
+ permissions: write-all
15
+ runs-on: codeberg-tiny
16
+ container:
17
+ image: docker.io/library/python:3-trixie
18
+ steps:
19
+ - name: Set up node
20
+ run: apt-get update && apt-get install -y --no-install-recommends nodejs
21
+
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e .
26
+
27
+ - name: Update DUT1 data
28
+ run: python -m wwvb.updateiers --dist
29
+
30
+ - name: Test
31
+ run: python -munittest discover -s test
32
+
33
+ - name: Commit updates
34
+ run: |
35
+ git config user.name "${FORGEJO_ACTOR} (actions cron)"
36
+ git config user.email "${FORGEJO_ACTOR}@noreply.invalid"
37
+ if git commit -m"update iersdata" src/wwvb/iersdata.json; then git push origin; fi
@@ -10,30 +10,25 @@ on:
10
10
 
11
11
  jobs:
12
12
  release:
13
-
14
- runs-on: ubuntu-24.04
13
+ permissions: write-all
14
+ runs-on: codeberg-tiny
15
+ container:
16
+ image: docker.io/library/python:3-trixie
15
17
  steps:
16
- - name: Dump GitHub context
17
- env:
18
- GITHUB_CONTEXT: ${{ toJson(github) }}
19
- run: echo "$GITHUB_CONTEXT"
18
+ - name: Set up node
19
+ run: apt-get update && apt-get install -y --no-install-recommends nodejs
20
20
 
21
21
  - uses: actions/checkout@v4
22
22
  with:
23
23
  persist-credentials: false
24
24
 
25
- - name: Set up Python
26
- uses: actions/setup-python@v5
27
- with:
28
- python-version: 3.9
29
-
30
25
  - name: Install deps
31
26
  run: |
32
27
  python -mpip install wheel
33
28
  python -mpip install -r requirements-dev.txt
34
29
 
35
30
  - name: Test
36
- run: make coverage
31
+ run: make coverage mypy
37
32
 
38
33
  - name: Build release
39
34
  run: python -mbuild
@@ -42,4 +37,4 @@ jobs:
42
37
  run: twine upload -u "$TWINE_USERNAME" -p "$TWINE_PASSWORD" dist/*
43
38
  env:
44
39
  TWINE_USERNAME: __token__
45
- TWINE_PASSWORD: ${{ secrets.pypi_token }}
40
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
@@ -9,6 +9,7 @@
9
9
  /build
10
10
  /_build
11
11
  /coverage.xml
12
+ /coverage.json
12
13
  /dist
13
14
  /finals2000A.all.csv
14
15
  /htmlcov
@@ -21,10 +21,16 @@ repos:
21
21
  - id: reuse
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
23
  # Ruff version.
24
- rev: v0.12.10
24
+ rev: v0.12.11
25
25
  hooks:
26
26
  # Run the linter.
27
- - id: ruff
27
+ - id: ruff-check
28
28
  args: [ --fix ]
29
29
  # Run the formatter.
30
30
  - id: ruff-format
31
+ - repo: https://github.com/asottile/pyupgrade
32
+ rev: v3.20.0
33
+ hooks:
34
+ - id: pyupgrade
35
+ args: [ --py310-plus ]
36
+ exclude: src/uwwvb.py # CircuitPython prevailing standard!
@@ -24,7 +24,7 @@ ENVPYTHON ?= _env/bin/python3
24
24
  endif
25
25
 
26
26
  .PHONY: default
27
- default: coverage mypy
27
+ default: coverage mypy pyright pyrefly
28
28
 
29
29
  COVERAGE_INCLUDE=--include "src/**/*.py"
30
30
  .PHONY: coverage
@@ -1,19 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wwvb
3
- Version: 7.0.0
3
+ Version: 8.0.0rc2
4
4
  Summary: Generate WWVB timecodes for any desired time
5
5
  Author-email: Jeff Epler <jepler@gmail.com>
6
- Project-URL: Source, https://github.com/jepler/wwvbpy
7
- Project-URL: Documentation, https://github.com/jepler/wwvbpy
6
+ Project-URL: Source, https://codeberg.org/jepler/wwvbpy
7
+ Project-URL: Documentation, https://wwvbpy.readthedocs.io/en/latest/
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
9
  Classifier: Programming Language :: Python :: Implementation :: PyPy
13
10
  Classifier: Programming Language :: Python :: Implementation :: CPython
14
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
12
  Classifier: Operating System :: OS Independent
16
- Requires-Python: >=3.9
13
+ Requires-Python: >=3.10
17
14
  Description-Content-Type: text/markdown
18
15
  Requires-Dist: adafruit-circuitpython-datetime
19
16
  Requires-Dist: beautifulsoup4
@@ -29,11 +26,7 @@ SPDX-FileCopyrightText: 2021-2024 Jeff Epler
29
26
 
30
27
  SPDX-License-Identifier: GPL-3.0-only
31
28
  -->
32
- [![Test wwvbgen](https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
33
- [![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
34
- [![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
35
29
  [![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
36
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
37
30
 
38
31
  # Purpose
39
32
 
@@ -63,7 +56,7 @@ The package includes:
63
56
 
64
57
  # Development status
65
58
 
66
- The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
59
+ The author ([@jepler](https://unpythonic.net)) occasionally develops and maintains this project, but
67
60
  issues are not likely to be acted on. They would be interested in adding
68
61
  co-maintainer(s).
69
62
 
@@ -3,11 +3,7 @@ SPDX-FileCopyrightText: 2021-2024 Jeff Epler
3
3
 
4
4
  SPDX-License-Identifier: GPL-3.0-only
5
5
  -->
6
- [![Test wwvbgen](https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
7
- [![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
8
- [![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
9
6
  [![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
10
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
11
7
 
12
8
  # Purpose
13
9
 
@@ -37,7 +33,7 @@ The package includes:
37
33
 
38
34
  # Development status
39
35
 
40
- The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
36
+ The author ([@jepler](https://unpythonic.net)) occasionally develops and maintains this project, but
41
37
  issues are not likely to be acted on. They would be interested in adding
42
38
  co-maintainer(s).
43
39
 
@@ -29,18 +29,15 @@ description = "Generate WWVB timecodes for any desired time"
29
29
  dynamic = ["readme","version","dependencies"]
30
30
  classifiers = [
31
31
  "Programming Language :: Python :: 3",
32
- "Programming Language :: Python :: 3.9",
33
- "Programming Language :: Python :: 3.10",
34
- "Programming Language :: Python :: 3.11",
35
32
  "Programming Language :: Python :: Implementation :: PyPy",
36
33
  "Programming Language :: Python :: Implementation :: CPython",
37
34
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
38
35
  "Operating System :: OS Independent",
39
36
  ]
40
- requires-python = ">=3.9"
37
+ requires-python = ">=3.10"
41
38
  [project.urls]
42
- Source = "https://github.com/jepler/wwvbpy"
43
- Documentation = "https://github.com/jepler/wwvbpy"
39
+ Source = "https://codeberg.org/jepler/wwvbpy"
40
+ Documentation = "https://wwvbpy.readthedocs.io/en/latest/"
44
41
  [project.scripts]
45
42
  wwvbgen = "wwvb.gen:main"
46
43
  wwvbdecode = "wwvb.decode:main"
@@ -7,10 +7,12 @@ build
7
7
  click
8
8
  coverage >= 7.10.3
9
9
  mypy; implementation_name=="cpython"
10
+ pyright; implementation_name=="cpython"
11
+ pyrefly; implementation_name=="cpython"
10
12
  click>=8.1.5; implementation_name=="cpython"
11
13
  leapseconddata
12
14
  platformdirs
13
- pre-commit
15
+ prek
14
16
  python-dateutil
15
17
  requests; implementation_name=="cpython"
16
18
  setuptools>=68; implementation_name=="cpython"
@@ -20,15 +20,30 @@ import enum
20
20
  import json
21
21
  import warnings
22
22
  from dataclasses import dataclass
23
- from typing import ClassVar
23
+ from typing import ClassVar, Literal
24
24
 
25
25
  from . import iersdata
26
26
  from .tz import Mountain
27
27
 
28
+ WWVBChannel = Literal["amplitude", "phase", "both"]
29
+
28
30
  TYPE_CHECKING = False
29
31
  if TYPE_CHECKING:
30
32
  from collections.abc import Generator
31
- from typing import Any, Self, TextIO, TypeVar
33
+ from typing import NotRequired, Self, TextIO, TypedDict, TypeVar
34
+
35
+ class JsonMinute(TypedDict):
36
+ """Implementation detail
37
+
38
+ This is the Python object type that is serialized by `print_timecodes_json`
39
+ """
40
+
41
+ year: int
42
+ days: int
43
+ hour: int
44
+ minute: int
45
+ amplitude: NotRequired[str]
46
+ phase: NotRequired[str]
32
47
 
33
48
  T = TypeVar("T")
34
49
 
@@ -787,7 +802,12 @@ class WWVBMinuteIERS(WWVBMinute):
787
802
 
788
803
 
789
804
  def _bcd_bits(n: int) -> Generator[bool]:
790
- """Return the bcd representation of n, starting with the least significant bit"""
805
+ """Return the (infinite) bcd representation of n, starting with the least significant bit
806
+
807
+ This continues to yield `False` forever after the nonzero bcd digits have been genrated.
808
+ This allows to `zip(strict=False)` the resulting bits together with a sequence of indices
809
+ and ensure all the upper bits are set to zero.
810
+ """
791
811
  while True:
792
812
  d = n % 10
793
813
  n = n // 10
@@ -866,7 +886,7 @@ class WWVBTimecode:
866
886
  Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number
867
887
  """
868
888
  pos = list(poslist)[::-1]
869
- for p, b in zip(pos, _bcd_bits(v)):
889
+ for p, b in zip(pos, _bcd_bits(v), strict=False):
870
890
  if b:
871
891
  self.am[p] = AmplitudeModulation.ONE
872
892
  else:
@@ -894,7 +914,7 @@ class WWVBTimecode:
894
914
  return ("⁰", "¹", "²", "¿")[am]
895
915
  return ("₀", "₁", "₂", "⸮")[am]
896
916
 
897
- return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase))
917
+ return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase, strict=True))
898
918
 
899
919
  def __repr__(self) -> str:
900
920
  """Implement repr()"""
@@ -912,7 +932,7 @@ class WWVBTimecode:
912
932
 
913
933
  def to_both_string(self, charset: list[str]) -> str:
914
934
  """Convert both channels to a string"""
915
- return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase))
935
+ return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase, strict=True))
916
936
 
917
937
 
918
938
  styles = {
@@ -927,7 +947,7 @@ styles = {
927
947
  def print_timecodes(
928
948
  w: WWVBMinute,
929
949
  minutes: int,
930
- channel: str,
950
+ channel: WWVBChannel,
931
951
  style: str,
932
952
  file: TextIO,
933
953
  *,
@@ -964,7 +984,7 @@ def print_timecodes(
964
984
  def print_timecodes_json(
965
985
  w: WWVBMinute,
966
986
  minutes: int,
967
- channel: str,
987
+ channel: WWVBChannel,
968
988
  file: TextIO,
969
989
  ) -> None:
970
990
  """Print a range of timecodes in JSON format.
@@ -984,7 +1004,7 @@ def print_timecodes_json(
984
1004
  """
985
1005
  result = []
986
1006
  for _ in range(minutes):
987
- data: dict[str, Any] = {
1007
+ data: JsonMinute = {
988
1008
  "year": w.year,
989
1009
  "days": w.days,
990
1010
  "hour": w.hour,
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '7.0.0'
32
- __version_tuple__ = version_tuple = (7, 0, 0)
31
+ __version__ = version = '8.0.0rc2'
32
+ __version_tuple__ = version_tuple = (8, 0, 0, 'rc2')
33
33
 
34
- __commit_id__ = commit_id = 'g14c182717'
34
+ __commit_id__ = commit_id = 'g65bfd773d'
@@ -6,10 +6,10 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import sys
9
- from typing import TYPE_CHECKING
10
9
 
11
10
  import wwvb
12
11
 
12
+ TYPE_CHECKING = False
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Generator
15
15
 
@@ -9,15 +9,18 @@ from __future__ import annotations
9
9
 
10
10
  import datetime
11
11
  import sys
12
- from typing import Any
13
12
 
14
13
  import click
15
14
  import dateutil.parser
16
15
 
17
16
  from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
18
17
 
18
+ TYPE_CHECKING = False
19
+ if TYPE_CHECKING:
20
+ from . import WWVBChannel
19
21
 
20
- def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
22
+
23
+ def parse_timespec(ctx: click.Context, param: click.Parameter, value: list[str]) -> datetime.datetime: # noqa: ARG001
21
24
  """Parse a time specifier from the commandline"""
22
25
  try:
23
26
  if len(value) == 5:
@@ -95,7 +98,7 @@ def main(
95
98
  dut1: int,
96
99
  minutes: int,
97
100
  style: str,
98
- channel: str,
101
+ channel: WWVBChannel,
99
102
  all_timecodes: bool,
100
103
  timespec: datetime.datetime,
101
104
  ) -> None:
@@ -1 +1 @@
1
- {"START": "1972-01-01", "OFFSETS_GZ": "H4sIAOvijWgC/+2aa3LDMAiEL5uHLTuxnN5/pn/aTmfSSiAWhGy+E2SWZQE58zwiH/1YivB/96vMXiIX2Io8CTyIrDSWGqlMRdrpDa6aJFnr0m4wYZkCE2UmSF0V+13vBveStK6JTfQyW3O86HLJf0RvDgy5u4FCI+WVKTsVoUdHzsrRoWRfYHIItZ5EEgu0Beu58EgEpMpO9zf4/s3iNO4y7/hqEwOZIPu3+PuO2T7Ic5E8GxsnZHvUYOtELxW1WP+0yx/caFxpyAooq6lq06UEr+UkLeXOIDPZ6EBrqb5K8Tvu6/B9CdnZqFQL05s2KauWy/IeF/tJGAisjK9MgGyDuUkRq4G1gRE+VjA30uZNPsdantkgMq58QO4fw+sqzj+A2/16mmvnyy9UzDvMktDgKYlnkFeB2rx+wNANG40aA4OgsY03AWoDCVs/XMmkyQ0+0jWaUqPdwA0m/MRuccGjCwirHToWzbcs8P7U1nZZLSYdHapWu5HqVg1YjK2fPEwvPZPzLPUF848tyid2u7dh8B7h+wVQ923Q+kqxZe3JclSSB+YTM3nnHrjgFth/vzgZzw6cbOMYa4bHFPU/DR3mp/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V+zUyyvbb/gE8h8bi3oTQAA"}
1
+ {"START": "1972-01-01", "OFFSETS_GZ": "H4sIAJLBtmgC/+2aa3LDMAiEL5uHLTuxnN5/pn/aTmfSSiAWhGy+E2SWZQE58zwiH/1YivB/96vMXiIX2Io8CTyIrDSWGqlMRdrpDa6aJFnr0m4wYZkCE2UmSF0V+13vBveStK6JTfQyW3O86HLJf0RvDgy5u4FCI+WVKTsVoUdHzsrRoWRfYHIItZ5EEgu0Beu58EgEpMpO9zf4/s3iNO4y7/hqEwOZIPu3+PuO2T7Ic5E8GxsnZHvUYOtELxW1WP+0yx/caFxpyAooq6lq06UEr+UkLeXOIDPZ6EBrqb5K8Tvu6/B9CdnZqFQL05s2KauWy/IeF/tJGAisjK9MgGyDuUkRq4G1gRE+VjA30uZNPsdantkgMq58QO4fw+sqzj+A2/16mmvnyy9UzDvMktDgKYlnkFeB2rx+wNANG40aA4OgsY03AWoDCVs/XMmkyQ0+0jWaUqPdwA0m/MRuccGjCwirHToWzbcs8P7U1nZZLSYdHapWu5HqVg1YjK2fPEwvPZPzLPUF848tyid2u7dh8B7h+wVQ923Q+kqxZe3JclSSB+YTM3nnHrjgFth/vzgZzw6cbOMYa4bHFPU/DR3mp/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V+z0yyz2F/zCTF9VskETgAA"}
@@ -15,13 +15,16 @@ import gzip
15
15
  import io
16
16
  import json
17
17
  import pathlib
18
- from typing import Callable
18
+ from typing import TYPE_CHECKING
19
19
 
20
20
  import bs4
21
21
  import click
22
22
  import platformdirs
23
23
  import requests
24
24
 
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable
27
+
25
28
  DIST_PATH = pathlib.Path(__file__).parent / "iersdata.json"
26
29
 
27
30
  IERS_URL = "https://datacenter.iers.org/data/csv/finals2000A.all.csv"
@@ -8,13 +8,13 @@ from __future__ import annotations
8
8
 
9
9
  import datetime
10
10
  import functools
11
- from tkinter import Canvas, TclError, Tk
12
- from typing import TYPE_CHECKING, Any
11
+ from tkinter import Canvas, Event, TclError, Tk
13
12
 
14
13
  import click
15
14
 
16
15
  import wwvb
17
16
 
17
+ TYPE_CHECKING = False
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Generator
20
20
 
@@ -25,7 +25,7 @@ def _app() -> Tk:
25
25
  return Tk()
26
26
 
27
27
 
28
- def validate_colors(ctx: Any, param: Any, value: str) -> list[str]: # noqa: ARG001
28
+ def validate_colors(ctx: click.Context, param: click.Parameter, value: str) -> list[str]: # noqa: ARG001
29
29
  """Check that all colors in a string are valid, splitting it to a list"""
30
30
  app = _app()
31
31
  colors = value.split()
@@ -106,7 +106,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
106
106
  canvas.pack(fill="both", expand=True)
107
107
  app.wm_deiconify()
108
108
 
109
- def resize_canvas(event: Any) -> None:
109
+ def resize_canvas(event: Event) -> None:
110
110
  """Keep the circle filling the window when it is resized"""
111
111
  sz = min(event.width, event.height) - 8
112
112
  if sz < 0:
@@ -141,10 +141,12 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
141
141
 
142
142
  controller = controller_func().__next__
143
143
 
144
+ # pyrefly: ignore # bad-assignment
144
145
  def after_func() -> None:
145
146
  """Repeatedly run the controller after the desired interval"""
146
147
  app.after(controller(), after_func)
147
148
 
149
+ # pyrefly: ignore # bad-argument-type
148
150
  app.after_idle(after_func)
149
151
  app.mainloop()
150
152
 
@@ -1,19 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wwvb
3
- Version: 7.0.0
3
+ Version: 8.0.0rc2
4
4
  Summary: Generate WWVB timecodes for any desired time
5
5
  Author-email: Jeff Epler <jepler@gmail.com>
6
- Project-URL: Source, https://github.com/jepler/wwvbpy
7
- Project-URL: Documentation, https://github.com/jepler/wwvbpy
6
+ Project-URL: Source, https://codeberg.org/jepler/wwvbpy
7
+ Project-URL: Documentation, https://wwvbpy.readthedocs.io/en/latest/
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
9
  Classifier: Programming Language :: Python :: Implementation :: PyPy
13
10
  Classifier: Programming Language :: Python :: Implementation :: CPython
14
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
12
  Classifier: Operating System :: OS Independent
16
- Requires-Python: >=3.9
13
+ Requires-Python: >=3.10
17
14
  Description-Content-Type: text/markdown
18
15
  Requires-Dist: adafruit-circuitpython-datetime
19
16
  Requires-Dist: beautifulsoup4
@@ -29,11 +26,7 @@ SPDX-FileCopyrightText: 2021-2024 Jeff Epler
29
26
 
30
27
  SPDX-License-Identifier: GPL-3.0-only
31
28
  -->
32
- [![Test wwvbgen](https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
33
- [![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
34
- [![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
35
29
  [![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
36
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
37
30
 
38
31
  # Purpose
39
32
 
@@ -63,7 +56,7 @@ The package includes:
63
56
 
64
57
  # Development status
65
58
 
66
- The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
59
+ The author ([@jepler](https://unpythonic.net)) occasionally develops and maintains this project, but
67
60
  issues are not likely to be acted on. They would be interested in adding
68
61
  co-maintainer(s).
69
62
 
@@ -3,13 +3,12 @@
3
3
  .readthedocs.yaml
4
4
  Makefile
5
5
  README.md
6
- codecov.yml
7
6
  pyproject.toml
8
7
  requirements-dev.txt
9
8
  requirements.txt
10
- .github/workflows/cron.yml
11
- .github/workflows/release.yml
12
- .github/workflows/test.yml
9
+ .forgejo/workflows/ci.yml
10
+ .forgejo/workflows/cron.yml
11
+ .forgejo/workflows/release.yml
13
12
  LICENSES/CC0-1.0.txt
14
13
  LICENSES/GPL-3.0-only.txt
15
14
  LICENSES/Unlicense.txt
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/python3
2
+
2
3
  """Test most wwvblib commandline programs"""
3
4
 
4
5
  # ruff: noqa: N802 D102
@@ -6,13 +7,13 @@
6
7
  #
7
8
  # SPDX-License-Identifier: GPL-3.0-only
8
9
 
10
+ from __future__ import annotations
11
+
9
12
  import json
10
13
  import os
11
14
  import subprocess
12
15
  import sys
13
16
  import unittest
14
- from collections.abc import Sequence
15
- from typing import Any
16
17
 
17
18
  # These imports must remain, even though the module contents are not used directly!
18
19
  import wwvb.dut1table
@@ -22,6 +23,12 @@ import wwvb.gen
22
23
  assert wwvb.dut1table.__name__ == "wwvb.dut1table"
23
24
  assert wwvb.gen.__name__ == "wwvb.gen"
24
25
 
26
+ TYPE_CHECKING = False
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Sequence
29
+
30
+ from wwvb import JsonMinute
31
+
25
32
 
26
33
  class CLITestCase(unittest.TestCase):
27
34
  """Test various CLI commands within wwvbpy"""
@@ -55,9 +62,10 @@ class CLITestCase(unittest.TestCase):
55
62
  def assertStarts(self, expected: str, actual: str, *args: str) -> None:
56
63
  self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
57
64
 
58
- def assertModuleJson(self, expected: Any, *args: str) -> None:
65
+ def assertModuleJson(self, expected: list[JsonMinute], *args: str) -> None:
59
66
  """Check the output from invoking a `python -m modulename` program matches the expected"""
60
67
  actual = self.moduleOutput(*args)
68
+ # Note: in mypy, revealed type of json.loads is typing.Any!
61
69
  self.assertEqual(json.loads(actual), expected)
62
70
 
63
71
  def assertModuleOutputStarts(self, expected: str, *args: str) -> None:
@@ -56,7 +56,3 @@ class TestLeapSecond(unittest.TestCase):
56
56
  assert not our_is_ls
57
57
  d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
58
58
  self.assertEqual(leap, bench)
59
-
60
-
61
- if __name__ == "__main__":
62
- unittest.main()
@@ -27,7 +27,3 @@ class TestPhaseModulation(unittest.TestCase):
27
27
 
28
28
  self.assertEqual(ref_am, test_am)
29
29
  self.assertEqual(ref_pm, test_pm)
30
-
31
-
32
- if __name__ == "__main__":
33
- unittest.main()
@@ -10,26 +10,21 @@ import random
10
10
  import sys
11
11
  import unittest
12
12
  import zoneinfo
13
- from typing import Union
14
13
 
15
14
  import adafruit_datetime
16
15
 
17
16
  import uwwvb
18
17
  import wwvb
19
18
 
20
- EitherDatetimeOrNone = Union[None, datetime.datetime, adafruit_datetime.datetime]
21
-
22
19
 
23
20
  class WWVBRoundtrip(unittest.TestCase):
24
21
  """tests of uwwvb.py"""
25
22
 
26
- def assertDateTimeEqualExceptTzInfo(self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone) -> None:
23
+ def assertDateTimeEqualExceptTzInfo(self, a: datetime.datetime, b: adafruit_datetime.datetime) -> None:
27
24
  """Test two datetime objects for equality
28
25
 
29
26
  This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal
30
27
  """
31
- assert a
32
- assert b
33
28
  self.assertEqual(
34
29
  (a.year, a.month, a.day, a.hour, a.minute, a.second, a.microsecond),
35
30
  (b.year, b.month, b.day, b.hour, b.minute, b.second, b.microsecond),
@@ -47,7 +42,7 @@ class WWVBRoundtrip(unittest.TestCase):
47
42
  any_leap_second = False
48
43
  for _ in range(20):
49
44
  timecode = minute.as_timecode()
50
- decoded = None
45
+ decoded: uwwvb.WWVBMinute | None = None
51
46
  if len(timecode.am) == 61:
52
47
  any_leap_second = True
53
48
  for code in timecode.am:
@@ -215,7 +210,3 @@ class WWVBRoundtrip(unittest.TestCase):
215
210
  datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
216
211
  uwwvb.as_datetime_local(decoded),
217
212
  )
218
-
219
-
220
- if __name__ == "__main__":
221
- unittest.main()
@@ -18,7 +18,7 @@ import unittest
18
18
 
19
19
  import uwwvb
20
20
  import wwvb
21
- from wwvb import decode, iersdata, tz
21
+ from wwvb import WWVBChannel, decode, iersdata, tz
22
22
 
23
23
 
24
24
  class WWVBMinute2k(wwvb.WWVBMinute):
@@ -44,11 +44,15 @@ class WWVBTestCase(unittest.TestCase):
44
44
  header = lines[0].split()
45
45
  timestamp = " ".join(header[:10])
46
46
  options = header[10:]
47
- channel = "amplitude"
47
+ channel: WWVBChannel = "amplitude"
48
48
  style = "default"
49
49
  for o in options:
50
- if o.startswith("--channel="):
51
- channel = o[10:]
50
+ if o == "--channel=both":
51
+ channel = "both"
52
+ elif o == "--channel=amplitude":
53
+ channel = "amplitude"
54
+ elif o == "--channel=phase":
55
+ channel = "phase"
52
56
  elif o.startswith("--style="):
53
57
  style = o[8:]
54
58
  else:
@@ -430,7 +434,3 @@ class WWVBRoundtrip(unittest.TestCase):
430
434
  minute.am[57] = wwvb.AmplitudeModulation.MARK
431
435
  decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
432
436
  assert decoded_minute is None
433
-
434
-
435
- if __name__ == "__main__":
436
- unittest.main()
@@ -1,48 +0,0 @@
1
- # SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
- #
3
- # SPDX-License-Identifier: CC0-1.0
4
-
5
- name: Update DUT1 data
6
-
7
- on:
8
- schedule:
9
- - cron: '0 10 2 * *'
10
- workflow_dispatch:
11
-
12
- jobs:
13
- update-dut1:
14
- runs-on: ubuntu-24.04
15
- if: startswith(github.repository, 'jepler/')
16
- steps:
17
-
18
- - name: Dump GitHub context
19
- env:
20
- GITHUB_CONTEXT: ${{ toJson(github) }}
21
- run: echo "$GITHUB_CONTEXT"
22
-
23
- - uses: actions/checkout@v4
24
- with:
25
- persist-credentials: false
26
-
27
- - name: Set up Python 3.10
28
- uses: actions/setup-python@v5
29
- with:
30
- python-version: "3.10"
31
-
32
- - name: Install dependencies
33
- run: pip install -e .
34
-
35
- - name: Update DUT1 data
36
- run: python -m wwvb.updateiers --dist
37
-
38
- - name: Test
39
- run: python -munittest
40
-
41
- - name: Commit updates
42
- env:
43
- REPO: ${{ github.repository }}
44
- run: |
45
- git config user.name "${GITHUB_ACTOR} (github actions cron)"
46
- git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
47
- git remote set-url --push origin "https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/$REPO"
48
- if git commit -m"update iersdata" src/wwvb/iersdata.json; then git push origin HEAD:main; fi
@@ -1,126 +0,0 @@
1
- # SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
- #
3
- # SPDX-License-Identifier: CC0-1.0
4
-
5
- name: Test wwvbgen
6
-
7
- on:
8
- push:
9
- pull_request:
10
- release:
11
- types: [published]
12
- check_suite:
13
- types: [rerequested]
14
-
15
- jobs:
16
- docs:
17
- runs-on: ubuntu-latest
18
- steps:
19
- - name: Set up Python
20
- uses: actions/setup-python@v5
21
- with:
22
- python-version: '3.12'
23
-
24
- - uses: actions/checkout@v4
25
- with:
26
- persist-credentials: false
27
-
28
- - name: Install deps
29
- run: python -mpip install -r requirements-dev.txt
30
-
31
- - name: Build HTML docs
32
- run: make html
33
-
34
- typing:
35
- strategy:
36
- fail-fast: false
37
- matrix:
38
- python-version:
39
- - '3.13'
40
- os-version:
41
- - 'ubuntu-latest'
42
- runs-on: ${{ matrix.os-version }}
43
- steps:
44
- - uses: actions/checkout@v4
45
- with:
46
- persist-credentials: false
47
-
48
- - name: Set up Python
49
- uses: actions/setup-python@v5
50
- with:
51
- python-version: ${{ matrix.python-version }}
52
-
53
- - name: Install deps
54
- run: |
55
- python -mpip install wheel
56
- python -mpip install -r requirements-dev.txt
57
-
58
- - name: Check stubs
59
- if: (! startsWith(matrix.python-version, 'pypy-'))
60
- run: make mypy PYTHON=python
61
-
62
-
63
- test:
64
- strategy:
65
- fail-fast: false
66
- matrix:
67
- python-version:
68
- - '3.9'
69
- - '3.10'
70
- - '3.11'
71
- - '3.12'
72
- - '3.13'
73
- - '3.14.0-alpha.0 - 3.14'
74
- os-version:
75
- - 'ubuntu-latest'
76
- include:
77
- - os-version: 'macos-latest'
78
- python-version: '3.x'
79
- - os-version: 'windows-latest'
80
- python-version: '3.x'
81
- - os-version: 'ubuntu-latest'
82
- python-version: 'pypy-3.10'
83
-
84
- runs-on: ${{ matrix.os-version }}
85
- steps:
86
- - uses: actions/checkout@v4
87
- with:
88
- persist-credentials: false
89
-
90
- - name: Set up Python
91
- uses: actions/setup-python@v5
92
- with:
93
- python-version: ${{ matrix.python-version }}
94
-
95
- - name: Install deps
96
- run: |
97
- python -mpip install wheel
98
- python -mpip install -r requirements-dev.txt
99
-
100
- - name: Coverage
101
- run: make coverage PYTHON=python
102
-
103
- - name: Test installed version
104
- run: make test_venv PYTHON=python
105
-
106
- - name: Upload Coverage as artifact
107
- if: always()
108
- uses: actions/upload-artifact@v4
109
- with:
110
- name: coverage for ${{ matrix.python-version }} on ${{ matrix.os-version }}
111
- path: coverage.xml
112
-
113
- pre-commit:
114
- runs-on: ubuntu-latest
115
- steps:
116
- - uses: actions/checkout@v4
117
- with:
118
- persist-credentials: false
119
-
120
- - name: Set up Python
121
- uses: actions/setup-python@v5
122
- with:
123
- python-version: '3.x'
124
-
125
- - name: pre-commit
126
- run: pip install pre-commit && pre-commit run --all
wwvb-7.0.0/codecov.yml DELETED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes