pytekukko 0.16.0__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {pytekukko-0.16.0 → pytekukko-0.17.0}/CHANGELOG.md +21 -0
  2. {pytekukko-0.16.0/src/pytekukko.egg-info → pytekukko-0.17.0}/PKG-INFO +31 -8
  3. {pytekukko-0.16.0 → pytekukko-0.17.0}/README.md +27 -4
  4. {pytekukko-0.16.0 → pytekukko-0.17.0}/pyproject.toml +11 -13
  5. {pytekukko-0.16.0 → pytekukko-0.17.0}/requirements/dev-requirements.txt +2 -3
  6. pytekukko-0.17.0/requirements/test-requirements.txt +6 -0
  7. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/__init__.py +3 -3
  8. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/examples/print_collection_schedules.py +12 -3
  9. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/examples/print_invoice_headers.py +10 -3
  10. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/examples/print_next_collections.py +10 -3
  11. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/models.py +7 -7
  12. {pytekukko-0.16.0 → pytekukko-0.17.0/src/pytekukko.egg-info}/PKG-INFO +31 -8
  13. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko.egg-info/SOURCES.txt +4 -5
  14. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko.egg-info/entry_points.txt +0 -1
  15. pytekukko-0.17.0/src/pytekukko.egg-info/requires.txt +5 -0
  16. {pytekukko-0.16.0 → pytekukko-0.17.0}/tests/test_pytekukko.py +4 -4
  17. pytekukko-0.16.0/requirements/test-requirements.txt +0 -5
  18. pytekukko-0.16.0/src/pytekukko/examples/update_google_calendar.py +0 -211
  19. pytekukko-0.16.0/src/pytekukko.egg-info/requires.txt +0 -6
  20. {pytekukko-0.16.0 → pytekukko-0.17.0}/LICENSE +0 -0
  21. {pytekukko-0.16.0 → pytekukko-0.17.0}/MANIFEST.in +0 -0
  22. {pytekukko-0.16.0 → pytekukko-0.17.0}/setup.cfg +0 -0
  23. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/examples/__init__.py +0 -0
  24. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/exceptions.py +0 -0
  25. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko/py.typed +0 -0
  26. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko.egg-info/dependency_links.txt +0 -0
  27. {pytekukko-0.16.0 → pytekukko-0.17.0}/src/pytekukko.egg-info/top_level.txt +0 -0
  28. {pytekukko-0.16.0 → pytekukko-0.17.0}/tests/__init__.py +0 -0
  29. {pytekukko-0.16.0/tests/cassettes → pytekukko-0.17.0/tests/cassettes/test_pytekukko}/test_get_collection_schedule.yaml +0 -0
  30. {pytekukko-0.16.0/tests/cassettes → pytekukko-0.17.0/tests/cassettes/test_pytekukko}/test_get_invoice_headers.yaml +0 -0
  31. {pytekukko-0.16.0/tests/cassettes → pytekukko-0.17.0/tests/cassettes/test_pytekukko}/test_login_logout.yaml +0 -0
  32. {pytekukko-0.16.0/tests/cassettes → pytekukko-0.17.0/tests/cassettes/test_pytekukko}/test_logout.yaml +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.17.0](https://github.com/scop/pytekukko/compare/v0.16.0...v0.17.0) (2025-10-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * **examples:** facilitate shtab generated completions ([0b89ac6](https://github.com/scop/pytekukko/commit/0b89ac6b9d5c33e003f1a5e1bfa131e74625f5a1))
9
+ * remove Google calendar updater example ([c2f4c5c](https://github.com/scop/pytekukko/commit/c2f4c5c34824bcc3c91a2b7f82f57b971df98fe3))
10
+
11
+
12
+ ### Performance Improvements
13
+
14
+ * quote typing.cast type expressions ([76b5c56](https://github.com/scop/pytekukko/commit/76b5c566f49a2f385e89bbfaffaa3eb5dff1153e))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * add OpenSSF Scorecard badge ([a2b1fa8](https://github.com/scop/pytekukko/commit/a2b1fa800e8493e999afbb13b0834848bfd75ce2))
20
+ * add OpenSSF Scorecard badge, misc tweaks ([5b70504](https://github.com/scop/pytekukko/commit/5b70504b5ffca34158cda3cbf96f36f0ed70d82c))
21
+ * note Python in top level description ([adc916d](https://github.com/scop/pytekukko/commit/adc916d65a7a781071baa047e9478e87860acbff))
22
+ * **README:** update CI badge URLs ([084fc4b](https://github.com/scop/pytekukko/commit/084fc4bdc2c94d8c70655e44dbe3fd314d77f26a))
23
+
3
24
  ## [0.16.0](https://github.com/scop/pytekukko/compare/v0.15.0...v0.16.0) (2024-01-23)
4
25
 
5
26
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pytekukko
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: Jätekukko Omakukko API client
5
5
  Author-email: Ville Skyttä <ville.skytta@iki.fi>
6
6
  License:
@@ -222,16 +222,17 @@ License-File: LICENSE
222
222
  Requires-Dist: aiohttp~=3.4
223
223
  Provides-Extra: examples
224
224
  Requires-Dist: python-dotenv<2,>=0.10; extra == "examples"
225
- Requires-Dist: google-api-python-client>=2.0.2,~=2.0; extra == "examples"
226
- Requires-Dist: icalendar~=5.0; extra == "examples"
225
+ Requires-Dist: icalendar<7,>=5; extra == "examples"
226
+ Dynamic: license-file
227
227
 
228
228
  # pytekukko -- Jätekukko Omakukko API client
229
229
 
230
230
  [![Python versions](https://img.shields.io/pypi/pyversions/pytekukko.svg)](https://pypi.org/project/pytekukko/)
231
231
  [![PyPI version](https://badge.fury.io/py/pytekukko.svg)](https://badge.fury.io/py/pytekukko)
232
- [![CI status](https://github.com/scop/pytekukko/workflows/check/badge.svg)](https://github.com/scop/pytekukko/actions?query=workflow%3Acheck)
232
+ [![CI status](https://github.com/scop/pytekukko/actions/workflows/test.yaml/badge.svg)](https://github.com/scop/pytekukko/actions/workflows/test.yaml)
233
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/scop/pytekukko/badge)](https://scorecard.dev/viewer/?uri=github.com%2Fscop%2Fpytekukko)
233
234
 
234
- Simple asyncio client for the [Jätekukko](https://www.jatekukko.fi)
235
+ Simple Python asyncio client for the [Jätekukko](https://www.jatekukko.fi)
235
236
  [Omakukko](https://tilasto.jatekukko.fi/indexservice2.jsp) API.
236
237
 
237
238
  The API of this package is modeled closely after the Omakukko
@@ -254,6 +255,8 @@ detected. If the detection is successful, there is no need to
254
255
  separately track session expiration or use the `login` method in the
255
256
  first place.
256
257
 
258
+ ## Command line examples
259
+
257
260
  For usage examples, see utilities in the `pytekukko.examples`
258
261
  package. Executables and dependencies for these are installed when the
259
262
  package is installed with the `examples` extra, invoke them with
@@ -262,8 +265,28 @@ package is installed with the `examples` extra, invoke them with
262
265
  - `pytekukko-collection-schedules`: output collection schedules in JSON
263
266
  - `pytekukko-invoice-headers`: output basic info on invoices in JSON
264
267
  - `pytekukko-next-collections`: output next collection dates in JSON
265
- - `pytekukko-update-google-calendar`: update Google Calendar with
266
- events for next collections
268
+
269
+ Shell completions for the examples can be generated with
270
+ [shtab's CLI usage mode](https://docs.iterative.ai/shtab/use/#cli-usage).
271
+
272
+ <details>
273
+
274
+ ```shell
275
+ shtab \
276
+ --prog pytekukko-collection-schedules \
277
+ --prefix pytekukko_collection_schedules \
278
+ pytekukko.examples.print_collection_schedules.argparser
279
+ shtab \
280
+ --prog pytekukko-invoice-headers \
281
+ --prefix pytekukko_invoice_headers \
282
+ pytekukko.examples.print_invoice_headers.argparser
283
+ shtab \
284
+ --prog pytekukko-next-collections \
285
+ --prefix pytekukko_next_collections \
286
+ pytekukko.examples.print_next_collections.argparser
287
+ ```
288
+
289
+ </details>
267
290
 
268
291
  ## Disclaimer
269
292
 
@@ -2,9 +2,10 @@
2
2
 
3
3
  [![Python versions](https://img.shields.io/pypi/pyversions/pytekukko.svg)](https://pypi.org/project/pytekukko/)
4
4
  [![PyPI version](https://badge.fury.io/py/pytekukko.svg)](https://badge.fury.io/py/pytekukko)
5
- [![CI status](https://github.com/scop/pytekukko/workflows/check/badge.svg)](https://github.com/scop/pytekukko/actions?query=workflow%3Acheck)
5
+ [![CI status](https://github.com/scop/pytekukko/actions/workflows/test.yaml/badge.svg)](https://github.com/scop/pytekukko/actions/workflows/test.yaml)
6
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/scop/pytekukko/badge)](https://scorecard.dev/viewer/?uri=github.com%2Fscop%2Fpytekukko)
6
7
 
7
- Simple asyncio client for the [Jätekukko](https://www.jatekukko.fi)
8
+ Simple Python asyncio client for the [Jätekukko](https://www.jatekukko.fi)
8
9
  [Omakukko](https://tilasto.jatekukko.fi/indexservice2.jsp) API.
9
10
 
10
11
  The API of this package is modeled closely after the Omakukko
@@ -27,6 +28,8 @@ detected. If the detection is successful, there is no need to
27
28
  separately track session expiration or use the `login` method in the
28
29
  first place.
29
30
 
31
+ ## Command line examples
32
+
30
33
  For usage examples, see utilities in the `pytekukko.examples`
31
34
  package. Executables and dependencies for these are installed when the
32
35
  package is installed with the `examples` extra, invoke them with
@@ -35,8 +38,28 @@ package is installed with the `examples` extra, invoke them with
35
38
  - `pytekukko-collection-schedules`: output collection schedules in JSON
36
39
  - `pytekukko-invoice-headers`: output basic info on invoices in JSON
37
40
  - `pytekukko-next-collections`: output next collection dates in JSON
38
- - `pytekukko-update-google-calendar`: update Google Calendar with
39
- events for next collections
41
+
42
+ Shell completions for the examples can be generated with
43
+ [shtab's CLI usage mode](https://docs.iterative.ai/shtab/use/#cli-usage).
44
+
45
+ <details>
46
+
47
+ ```shell
48
+ shtab \
49
+ --prog pytekukko-collection-schedules \
50
+ --prefix pytekukko_collection_schedules \
51
+ pytekukko.examples.print_collection_schedules.argparser
52
+ shtab \
53
+ --prog pytekukko-invoice-headers \
54
+ --prefix pytekukko_invoice_headers \
55
+ pytekukko.examples.print_invoice_headers.argparser
56
+ shtab \
57
+ --prog pytekukko-next-collections \
58
+ --prefix pytekukko_next_collections \
59
+ pytekukko.examples.print_next_collections.argparser
60
+ ```
61
+
62
+ </details>
40
63
 
41
64
  ## Disclaimer
42
65
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytekukko"
7
- version = "0.16.0"
7
+ version = "0.17.0"
8
8
  description = "Jätekukko Omakukko API client"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,27 +23,31 @@ classifiers = [
23
23
  dependencies = ["aiohttp~=3.4"]
24
24
 
25
25
  [project.optional-dependencies]
26
- examples = ["python-dotenv>=0.10,<2", "google-api-python-client~=2.0,>=2.0.2", "icalendar~=5.0"]
26
+ examples = ["python-dotenv>=0.10,<2", "icalendar>=5,<7"]
27
27
 
28
28
  [project.scripts]
29
29
  pytekukko-collection-schedules = "pytekukko.examples.print_collection_schedules:main [examples]"
30
30
  pytekukko-invoice-headers = "pytekukko.examples.print_invoice_headers:main [examples]"
31
31
  pytekukko-next-collections = "pytekukko.examples.print_next_collections:main [examples]"
32
- pytekukko-update-google-calendar = "pytekukko.examples.update_google_calendar:main [examples]"
33
32
 
34
33
  [project.urls]
35
34
  Homepage = "https://github.com/scop/pytekukko"
36
35
  Changelog = "https://github.com/scop/pytekukko/blob/main/CHANGELOG.md"
37
36
 
38
37
  [tool.ruff]
38
+ fix = true
39
+ src = ["src", "tests"]
40
+
41
+ [tool.ruff.lint]
39
42
  select = ["ALL"]
40
43
  ignore = [
41
44
  "ANN", # Maybe sometime
42
45
  "D203", # Ping/pong with D211
43
46
  "D213", # Ping/pong with D212
44
- "TCH003", # Maybe sometime
47
+ "TC003", # Maybe sometime
45
48
  # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
46
49
  # (keep order of ignores here same as ^there for maintainability)
50
+ # https://github.com/astral-sh/ruff/issues/8175
47
51
  "W191",
48
52
  "E111",
49
53
  "E114",
@@ -59,17 +63,16 @@ ignore = [
59
63
  "ISC001",
60
64
  "ISC002",
61
65
  ]
62
- fix = true
63
66
  unfixable = ["T20"]
64
- src = ["src", "tests"]
65
67
 
66
- [tool.ruff.per-file-ignores]
68
+ [tool.ruff.lint.per-file-ignores]
67
69
  "tests/*.py" = ["S101"]
68
70
 
69
71
  [tool.mypy]
70
72
  python_version = "3.10"
71
73
  mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
72
- enable_error_code = "ignore-without-code,redundant-self,truthy-iterable"
74
+ # covered by ruff: unimported-reveal (F821), unused-awaitable (RUF006)
75
+ enable_error_code = "deprecated,exhaustive-match,explicit-override,ignore-without-code,possibly-undefined,redundant-expr,redundant-self,truthy-bool,truthy-iterable"
73
76
  strict = true
74
77
  warn_unreachable = true
75
78
  exclude = "^build/"
@@ -80,8 +83,3 @@ ignore_missing_imports = true
80
83
 
81
84
  [tool.pytest.ini_options]
82
85
  asyncio_mode = "auto"
83
- filterwarnings = [
84
- "error",
85
- # https://github.com/pytest-dev/pytest/issues/10977
86
- "default:(ast\\.(NameConstant|Str)|Attribute s) is deprecated:DeprecationWarning:_pytest.assertion.rewrite",
87
- ]
@@ -1,6 +1,5 @@
1
1
  -e .[examples]
2
2
  -r test-requirements.txt
3
3
 
4
- mypy==1.8.0
5
- nox
6
- ruff==0.1.14
4
+ mypy==1.18.2
5
+ nox[uv]
@@ -0,0 +1,6 @@
1
+ pytest>=6
2
+ pytest-asyncio>=0.25
3
+ pytest-recording
4
+ vcrpy==7.0.0
5
+ python-dotenv>=0.10,<2
6
+ shtab
@@ -13,7 +13,7 @@ from aiohttp import ClientResponse, ClientResponseError, ClientSession
13
13
  from .exceptions import UnexpectedResponseStructureError
14
14
  from .models import CustomerData, InvoiceHeader, Service
15
15
 
16
- __version__ = "0.16.0"
16
+ __version__ = "0.17.0"
17
17
  DEFAULT_BASE_URL = "https://tilasto.jatekukko.fi/jatekukko/"
18
18
 
19
19
  SERVICE_TIMEZONE = ZoneInfo("Europe/Helsinki")
@@ -77,7 +77,7 @@ class Pytekukko:
77
77
  params=params,
78
78
  )
79
79
 
80
- return cast(list[date], _unmarshal(response_data))
80
+ return cast("list[date]", _unmarshal(response_data))
81
81
 
82
82
  async def get_invoice_headers(self) -> list[InvoiceHeader]:
83
83
  """Get headers of available invoices."""
@@ -114,7 +114,7 @@ class Pytekukko:
114
114
  raise_for_status=True,
115
115
  ) as response:
116
116
  # NOTE(scop): could check that we got {"response":"OK"}
117
- return cast(dict[str, str], await response.json())
117
+ return cast("dict[str, str]", await response.json())
118
118
 
119
119
  async def logout(self) -> None:
120
120
  """Log out the current session."""
@@ -2,6 +2,7 @@
2
2
 
3
3
  """Print collection schedules."""
4
4
 
5
+ import argparse
5
6
  import asyncio
6
7
  import json
7
8
  import sys
@@ -13,13 +14,21 @@ import pytekukko
13
14
  from pytekukko.examples import example_argparser, example_client
14
15
 
15
16
 
16
- async def run_example() -> None:
17
- """Run the example."""
17
+ def argparser() -> argparse.ArgumentParser:
18
+ """Return an argument parser for the example.
19
+
20
+ This is a separate function to facilitate shtab generated completions.
21
+ """
18
22
  argparser = example_argparser(__doc__)
19
23
  argparser.add_argument(
20
24
  "-i", "--icalendar", action="store_true", help="output iCalendar"
21
25
  )
22
- args = argparser.parse_args()
26
+ return argparser
27
+
28
+
29
+ async def run_example() -> None:
30
+ """Run the example."""
31
+ args = argparser().parse_args()
23
32
 
24
33
  client, cookie_jar, cookie_jar_path = example_client(args)
25
34
 
@@ -2,17 +2,24 @@
2
2
 
3
3
  """Print basic info on invoices."""
4
4
 
5
+ import argparse
5
6
  import asyncio
6
7
  import json
7
8
 
8
9
  from pytekukko.examples import example_argparser, example_client
9
10
 
10
11
 
12
+ def argparser() -> argparse.ArgumentParser:
13
+ """Return an argument parser for the example.
14
+
15
+ This is a separate function to facilitate shtab generated completions.
16
+ """
17
+ return example_argparser(__doc__)
18
+
19
+
11
20
  async def run_example() -> None:
12
21
  """Run the example."""
13
- client, cookie_jar, cookie_jar_path = example_client(
14
- example_argparser(__doc__).parse_args(),
15
- )
22
+ client, cookie_jar, cookie_jar_path = example_client(argparser().parse_args())
16
23
 
17
24
  async with client.session:
18
25
  data = [
@@ -2,17 +2,24 @@
2
2
 
3
3
  """Print next collection dates."""
4
4
 
5
+ import argparse
5
6
  import asyncio
6
7
  import json
7
8
 
8
9
  from pytekukko.examples import example_argparser, example_client
9
10
 
10
11
 
12
+ def argparser() -> argparse.ArgumentParser:
13
+ """Return an argument parser for the example.
14
+
15
+ This is a separate function to facilitate shtab generated completions.
16
+ """
17
+ return example_argparser(__doc__)
18
+
19
+
11
20
  async def run_example() -> None:
12
21
  """Run the example."""
13
- client, cookie_jar, cookie_jar_path = example_client(
14
- example_argparser(__doc__).parse_args(),
15
- )
22
+ client, cookie_jar, cookie_jar_path = example_client(argparser().parse_args())
16
23
 
17
24
  async with client.session:
18
25
  data = [
@@ -21,12 +21,12 @@ class Service:
21
21
  @property
22
22
  def name(self) -> str:
23
23
  """Get service name."""
24
- return cast(str, self.raw_data["ASTNimi"])
24
+ return cast("str", self.raw_data["ASTNimi"])
25
25
 
26
26
  @property
27
27
  def pos(self) -> int:
28
28
  """Get "pos" value."""
29
- return cast(int, self.raw_data["ASTPos"])
29
+ return cast("int", self.raw_data["ASTPos"])
30
30
 
31
31
  @property
32
32
  def next_collection(self) -> date | None:
@@ -50,12 +50,12 @@ class CustomerData:
50
50
  @property
51
51
  def customer_number(self) -> str:
52
52
  """Get customer number."""
53
- return cast(str, self.raw_data["asiakasnro"])
53
+ return cast("str", self.raw_data["asiakasnro"])
54
54
 
55
55
  @property
56
56
  def name(self) -> str:
57
57
  """Get customer name."""
58
- return cast(str, self.raw_data["nimi"])
58
+ return cast("str", self.raw_data["nimi"])
59
59
 
60
60
 
61
61
  @dataclass
@@ -71,14 +71,14 @@ class InvoiceHeader:
71
71
  @property
72
72
  def name(self) -> str:
73
73
  """Get customer number."""
74
- return cast(str, self.raw_data["name"])
74
+ return cast("str", self.raw_data["name"])
75
75
 
76
76
  @property
77
77
  def due_date(self) -> date:
78
78
  """Get due date."""
79
- return cast(date, self.raw_data["dueDate"])
79
+ return cast("date", self.raw_data["dueDate"])
80
80
 
81
81
  @property
82
82
  def total(self) -> float:
83
83
  """Get total amount."""
84
- return cast(float, self.raw_data["total"])
84
+ return cast("float", self.raw_data["total"])
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pytekukko
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: Jätekukko Omakukko API client
5
5
  Author-email: Ville Skyttä <ville.skytta@iki.fi>
6
6
  License:
@@ -222,16 +222,17 @@ License-File: LICENSE
222
222
  Requires-Dist: aiohttp~=3.4
223
223
  Provides-Extra: examples
224
224
  Requires-Dist: python-dotenv<2,>=0.10; extra == "examples"
225
- Requires-Dist: google-api-python-client>=2.0.2,~=2.0; extra == "examples"
226
- Requires-Dist: icalendar~=5.0; extra == "examples"
225
+ Requires-Dist: icalendar<7,>=5; extra == "examples"
226
+ Dynamic: license-file
227
227
 
228
228
  # pytekukko -- Jätekukko Omakukko API client
229
229
 
230
230
  [![Python versions](https://img.shields.io/pypi/pyversions/pytekukko.svg)](https://pypi.org/project/pytekukko/)
231
231
  [![PyPI version](https://badge.fury.io/py/pytekukko.svg)](https://badge.fury.io/py/pytekukko)
232
- [![CI status](https://github.com/scop/pytekukko/workflows/check/badge.svg)](https://github.com/scop/pytekukko/actions?query=workflow%3Acheck)
232
+ [![CI status](https://github.com/scop/pytekukko/actions/workflows/test.yaml/badge.svg)](https://github.com/scop/pytekukko/actions/workflows/test.yaml)
233
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/scop/pytekukko/badge)](https://scorecard.dev/viewer/?uri=github.com%2Fscop%2Fpytekukko)
233
234
 
234
- Simple asyncio client for the [Jätekukko](https://www.jatekukko.fi)
235
+ Simple Python asyncio client for the [Jätekukko](https://www.jatekukko.fi)
235
236
  [Omakukko](https://tilasto.jatekukko.fi/indexservice2.jsp) API.
236
237
 
237
238
  The API of this package is modeled closely after the Omakukko
@@ -254,6 +255,8 @@ detected. If the detection is successful, there is no need to
254
255
  separately track session expiration or use the `login` method in the
255
256
  first place.
256
257
 
258
+ ## Command line examples
259
+
257
260
  For usage examples, see utilities in the `pytekukko.examples`
258
261
  package. Executables and dependencies for these are installed when the
259
262
  package is installed with the `examples` extra, invoke them with
@@ -262,8 +265,28 @@ package is installed with the `examples` extra, invoke them with
262
265
  - `pytekukko-collection-schedules`: output collection schedules in JSON
263
266
  - `pytekukko-invoice-headers`: output basic info on invoices in JSON
264
267
  - `pytekukko-next-collections`: output next collection dates in JSON
265
- - `pytekukko-update-google-calendar`: update Google Calendar with
266
- events for next collections
268
+
269
+ Shell completions for the examples can be generated with
270
+ [shtab's CLI usage mode](https://docs.iterative.ai/shtab/use/#cli-usage).
271
+
272
+ <details>
273
+
274
+ ```shell
275
+ shtab \
276
+ --prog pytekukko-collection-schedules \
277
+ --prefix pytekukko_collection_schedules \
278
+ pytekukko.examples.print_collection_schedules.argparser
279
+ shtab \
280
+ --prog pytekukko-invoice-headers \
281
+ --prefix pytekukko_invoice_headers \
282
+ pytekukko.examples.print_invoice_headers.argparser
283
+ shtab \
284
+ --prog pytekukko-next-collections \
285
+ --prefix pytekukko_next_collections \
286
+ pytekukko.examples.print_next_collections.argparser
287
+ ```
288
+
289
+ </details>
267
290
 
268
291
  ## Disclaimer
269
292
 
@@ -19,10 +19,9 @@ src/pytekukko/examples/__init__.py
19
19
  src/pytekukko/examples/print_collection_schedules.py
20
20
  src/pytekukko/examples/print_invoice_headers.py
21
21
  src/pytekukko/examples/print_next_collections.py
22
- src/pytekukko/examples/update_google_calendar.py
23
22
  tests/__init__.py
24
23
  tests/test_pytekukko.py
25
- tests/cassettes/test_get_collection_schedule.yaml
26
- tests/cassettes/test_get_invoice_headers.yaml
27
- tests/cassettes/test_login_logout.yaml
28
- tests/cassettes/test_logout.yaml
24
+ tests/cassettes/test_pytekukko/test_get_collection_schedule.yaml
25
+ tests/cassettes/test_pytekukko/test_get_invoice_headers.yaml
26
+ tests/cassettes/test_pytekukko/test_login_logout.yaml
27
+ tests/cassettes/test_pytekukko/test_logout.yaml
@@ -2,4 +2,3 @@
2
2
  pytekukko-collection-schedules = pytekukko.examples.print_collection_schedules:main [examples]
3
3
  pytekukko-invoice-headers = pytekukko.examples.print_invoice_headers:main [examples]
4
4
  pytekukko-next-collections = pytekukko.examples.print_next_collections:main [examples]
5
- pytekukko-update-google-calendar = pytekukko.examples.update_google_calendar:main [examples]
@@ -0,0 +1,5 @@
1
+ aiohttp~=3.4
2
+
3
+ [examples]
4
+ python-dotenv<2,>=0.10
5
+ icalendar<7,>=5
@@ -69,7 +69,7 @@ async def fixture_client() -> Pytekukko:
69
69
  )
70
70
 
71
71
 
72
- @pytest.mark.vcr()
72
+ @pytest.mark.vcr
73
73
  async def test_login_logout(client: Pytekukko) -> None:
74
74
  """Test login followed by logout."""
75
75
  async with client.session:
@@ -77,14 +77,14 @@ async def test_login_logout(client: Pytekukko) -> None:
77
77
  await client.logout() # No exception counts as success here
78
78
 
79
79
 
80
- @pytest.mark.vcr()
80
+ @pytest.mark.vcr
81
81
  async def test_logout(client: Pytekukko) -> None:
82
82
  """Test bare logout."""
83
83
  async with client.session:
84
84
  await client.logout() # No exception counts as success here
85
85
 
86
86
 
87
- @pytest.mark.vcr()
87
+ @pytest.mark.vcr
88
88
  async def test_get_collection_schedule(client: Pytekukko) -> None:
89
89
  """Test getting collection schedule."""
90
90
  async with client.session:
@@ -95,7 +95,7 @@ async def test_get_collection_schedule(client: Pytekukko) -> None:
95
95
  assert all(isinstance(date, datetime.date) for date in dates)
96
96
 
97
97
 
98
- @pytest.mark.vcr()
98
+ @pytest.mark.vcr
99
99
  async def test_get_invoice_headers(client: Pytekukko) -> None:
100
100
  """Test getting invoice headers."""
101
101
  async with client.session:
@@ -1,5 +0,0 @@
1
- pytest>=6
2
- pytest-asyncio>=0.17
3
- pytest-vcr
4
- vcrpy==6.0.0
5
- python-dotenv>=0.10,<2
@@ -1,211 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- """Update Google Calendar with events for next collections.
4
-
5
- To get the required service account file, enable the Google Calendar API
6
- in the Google Cloud console, create service account credentials with
7
- access to it, and create keys of type JSON.
8
-
9
- Calendar id is typically the target Google Calendar account e-mail
10
- address.
11
- """
12
-
13
- import asyncio
14
- import datetime
15
- import logging
16
- import sys
17
- import zoneinfo
18
- from collections.abc import Mapping
19
- from typing import Any, NamedTuple, cast
20
-
21
- from google.oauth2 import service_account # type: ignore[import-untyped]
22
- from googleapiclient.discovery import build # type: ignore[import-untyped]
23
-
24
- from pytekukko.examples import arg_environ_default, example_argparser, example_client
25
-
26
- logging.basicConfig(level=logging.INFO)
27
- LOGGER = logging.getLogger(__name__)
28
-
29
-
30
- class CalendarData(NamedTuple):
31
- """Helper container for calendar data to update."""
32
-
33
- name: str
34
- date: datetime.date
35
- location: str
36
-
37
-
38
- # https://developers.google.com/resources/api-libraries/documentation/calendar/v3/python/latest/calendar_v3.events.html
39
- def update_google_calendar(
40
- credentials: service_account.Credentials,
41
- calendar_id: str,
42
- data: Mapping[str, CalendarData],
43
- ) -> None:
44
- """Update the calendar."""
45
- service = build(
46
- "calendar",
47
- "v3",
48
- credentials=credentials,
49
- # https://github.com/googleapis/google-api-python-client/issues/299
50
- # https://github.com/googleapis/google-api-python-client/issues/325
51
- cache_discovery=False,
52
- )
53
-
54
- # Events with only date set are apparently treated as occurring at midnight,
55
- # start of day in calendar's timezone.
56
- calendar = service.calendars().get(calendarId=calendar_id).execute()
57
- if calendar.get("timeZone"):
58
- timezone: datetime.tzinfo = zoneinfo.ZoneInfo(calendar["timeZone"])
59
- else:
60
- timezone = datetime.timezone.utc
61
- since = datetime.datetime.now(timezone).replace(hour=0) - datetime.timedelta(
62
- hours=1,
63
- )
64
-
65
- events = (
66
- service.events()
67
- .list(
68
- calendarId=calendar_id,
69
- orderBy="startTime",
70
- singleEvents=True,
71
- timeMin=since.isoformat(),
72
- privateExtendedProperty="pytekukko-managed=true",
73
- )
74
- .execute()
75
- )
76
-
77
- pos_events: dict[str, dict[str, Any]] = {}
78
- for event in events["items"]:
79
- # Store first event for each pos for update, delete rest
80
- pos = event["extendedProperties"]["private"].get("pytekukko-pos")
81
- if pos and pos in data and not pos_events.get(pos):
82
- pos_events[pos] = event
83
- else:
84
- service.events().delete(
85
- calendarId=calendar_id,
86
- eventId=event["id"],
87
- ).execute()
88
-
89
- for pos, pos_event_data in data.items():
90
- date = pos_event_data.date.isoformat()
91
- name = pos_event_data.name or ""
92
- description = f"{name} [pos={pos}]".strip()
93
- event_data = {
94
- "summary": "Jätekukko collection",
95
- "description": description,
96
- "location": pos_event_data.location or None,
97
- "start": {"date": date},
98
- "end": {"date": date},
99
- # Reminders are "for the authenticated user" per docs.
100
- # So for the service account, not the calendar owner :(
101
- # No way to set this for the "actual" calendar user from here,
102
- # at least while using a service account.
103
- "reminders": {"useDefault": False},
104
- "transparency": "transparent",
105
- "extendedProperties": {
106
- "private": {"pytekukko-managed": "true", "pytekukko-pos": pos},
107
- },
108
- "source": {
109
- "url": "https://tilasto.jatekukko.fi/indexservice2.jsp",
110
- "title": "Omakukko",
111
- },
112
- "creator": {
113
- "displayName": "Pytekukko",
114
- },
115
- }
116
-
117
- event = pos_events.get(pos)
118
-
119
- method = None
120
- log_level = logging.INFO
121
- if event:
122
- for key, value in event_data.items():
123
- if event.get(key) != value:
124
- action = "updated"
125
- method = service.events().patch(
126
- calendarId=calendar_id,
127
- eventId=event["id"],
128
- body=event_data,
129
- )
130
- break
131
- else:
132
- action = "unchanged"
133
- log_level = logging.DEBUG
134
- else:
135
- action = "created"
136
- method = service.events().insert(
137
- calendarId=calendar_id,
138
- body=event_data,
139
- )
140
- if method:
141
- event = method.execute()
142
- LOGGER.log(log_level, "Event %s: %s", action, event.get("htmlLink"))
143
-
144
-
145
- async def run_example() -> None:
146
- """Run the example."""
147
- argparser = example_argparser(__doc__)
148
- argparser.add_argument(
149
- "--google-calendar-id",
150
- type=str,
151
- **arg_environ_default("PYTEKUKKO_GOOGLE_CALENDAR_ID"), # type: ignore[arg-type]
152
- )
153
- argparser.add_argument(
154
- "--google-service-account-file",
155
- type=str,
156
- **arg_environ_default( # type: ignore[arg-type]
157
- "PYTEKUKKO_GOOGLE_SERVICE_ACCOUNT_FILE",
158
- fallback="service_account.json",
159
- ),
160
- )
161
- args = argparser.parse_args()
162
-
163
- if not args.google_calendar_id:
164
- print("Google calendar id required", file=sys.stderr) # noqa: T201
165
- sys.exit(2)
166
-
167
- client, cookie_jar, cookie_jar_path = example_client(args)
168
-
169
- credentials = service_account.Credentials.from_service_account_file(
170
- args.google_service_account_file,
171
- )
172
-
173
- async with client.session:
174
- services = await client.get_services()
175
- if not cookie_jar_path:
176
- await client.logout()
177
-
178
- if cookie_jar_path:
179
- cookie_jar.save(cookie_jar_path)
180
-
181
- data = {}
182
- for service in (x for x in services if x.next_collection):
183
- data[str(service.pos)] = CalendarData(
184
- name=service.name,
185
- date=cast(datetime.date, service.next_collection),
186
- location=", ".join(
187
- x
188
- for x in (
189
- service.raw_data.get("owner", {}).get("katu"),
190
- service.raw_data.get("owner", {}).get("posti"),
191
- )
192
- if x
193
- ),
194
- )
195
-
196
- await asyncio.get_event_loop().run_in_executor(
197
- None,
198
- update_google_calendar,
199
- credentials,
200
- args.google_calendar_id,
201
- data,
202
- )
203
-
204
-
205
- def main() -> None:
206
- """Run example in event loop."""
207
- asyncio.run(run_example())
208
-
209
-
210
- if __name__ == "__main__":
211
- main()
@@ -1,6 +0,0 @@
1
- aiohttp~=3.4
2
-
3
- [examples]
4
- python-dotenv<2,>=0.10
5
- google-api-python-client>=2.0.2,~=2.0
6
- icalendar~=5.0
File without changes
File without changes
File without changes
File without changes