offspot-config 2.0.0__tar.gz → 2.2.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 (58) hide show
  1. {offspot_config-2.0.0 → offspot_config-2.2.0}/CHANGELOG.md +35 -0
  2. {offspot_config-2.0.0 → offspot_config-2.2.0}/PKG-INFO +1 -1
  3. offspot_config-2.2.0/src/offspot_config/branding/horizontal-logo-light.png +0 -0
  4. offspot_config-2.2.0/src/offspot_config/branding/square-logo-light.png +0 -0
  5. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/builder.py +131 -31
  6. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/constants.py +1 -0
  7. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/file.py +19 -9
  8. offspot_config-2.2.0/src/offspot_config/inputs/base.py +51 -0
  9. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/checksum.py +4 -1
  10. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/mainconfig.py +1 -1
  11. offspot_config-2.2.0/src/offspot_config/inputs/ways.py +1 -0
  12. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/dashboard.py +12 -2
  13. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/misc.py +11 -0
  14. offspot_config-2.2.0/src/offspot_runtime/__about__.py +1 -0
  15. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/conftest.py +1 -1
  16. offspot_config-2.2.0/tests/test_inputs.py +92 -0
  17. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_reader.py +79 -18
  18. offspot_config-2.0.0/src/offspot_config/inputs/base.py +0 -30
  19. offspot_config-2.0.0/src/offspot_config/inputs/ways.py +0 -1
  20. offspot_config-2.0.0/src/offspot_runtime/__about__.py +0 -1
  21. offspot_config-2.0.0/tests/test_inputs.py +0 -8
  22. {offspot_config-2.0.0 → offspot_config-2.2.0}/.gitignore +0 -0
  23. {offspot_config-2.0.0 → offspot_config-2.2.0}/.pre-commit-config.yaml +0 -0
  24. {offspot_config-2.0.0 → offspot_config-2.2.0}/LICENSE +0 -0
  25. {offspot_config-2.0.0 → offspot_config-2.2.0}/README.md +0 -0
  26. {offspot_config-2.0.0 → offspot_config-2.2.0}/pyproject.toml +0 -0
  27. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/__init__.py +0 -0
  28. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/catalog.json +0 -0
  29. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/catalog.py +0 -0
  30. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/__init__.py +0 -0
  31. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/file.py +0 -0
  32. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/oci.py +0 -0
  33. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/output.py +0 -0
  34. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/str.py +0 -0
  35. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/oci_images.py +0 -0
  36. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/packages.py +0 -0
  37. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/__init__.py +0 -0
  38. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/download.py +0 -0
  39. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/sizes.py +0 -0
  40. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/yaml.py +0 -0
  41. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/zim.py +0 -0
  42. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/__init__.py +0 -0
  43. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/ap.py +0 -0
  44. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/checks.py +0 -0
  45. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/configlib.py +0 -0
  46. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/containers.py +0 -0
  47. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/dnsmasqspoof.py +0 -0
  48. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/ethernet.py +0 -0
  49. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/firmware.py +0 -0
  50. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/fromfile.py +0 -0
  51. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/hostname.py +0 -0
  52. {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/timezone.py +0 -0
  53. {offspot_config-2.0.0 → offspot_config-2.2.0}/tasks.py +0 -0
  54. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_catalog.py +0 -0
  55. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_checks.py +0 -0
  56. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_humanid.py +0 -0
  57. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_link.py +0 -0
  58. {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_zim.py +0 -0
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.2.0] - 2024-04-24
9
+
10
+ ### Added
11
+
12
+ - [inputs.file] `File.is_base64_encoded` if `via` is `base64`
13
+ - [utils.misc] `b64_encode()` and `b64_decode()` for reproducible base64 transport
14
+ - [branding] `branding` folder in `offspot_config` containing official/original offspot branding files
15
+ - [constants] `INTERNAL_BRANDING_PATH` pointing to code-reachable folder of branding files
16
+ - [builder] `ORIGINAL_BRANDING_PATH` constant for '/data/branding`
17
+ - [builder] `BRANDING_PATH` constant for `/data/contents/branding`
18
+
19
+ ### Changed
20
+
21
+ - [builder] Using kiwix-serve 3.7.0-1
22
+ - [builder] Using reverse-proxy 1.8
23
+ - [builder] Original branding files copied to `ORIGINAL_BRANDING_PATH`
24
+ - [builder] Creating empty `BRANDING_PATH` for hotspot-specific branding
25
+ - [builder] Added mounts for `BRANDING_PATH` (and `ORIGINAL_BRANDING_PATH`) on all internal apps
26
+ - [builder] Catalog apps can mount `${BRANDING_PATH}` or `${ORIGINAL_BRANDING_PATH}`
27
+
28
+ ## [2.1.0] - 2024-04-18
29
+
30
+ ### Added
31
+
32
+ - [inputs.checksum] `Checksum` gets a `to_dict()` method
33
+ - [dashboard] `Reader` gets an optional `checksum`
34
+
35
+ ### Changed
36
+
37
+ - [builder] Removed useless download.kiwix.org special behavior for reader checksum. Checksum now on Reader
38
+ - [builder] Using captive-portal 1.4.1
39
+ - [inputs.base] Version-only base image def now targets uncompressed URL
40
+ - [inputs.base] BaseConfig gets an optional `checksum` and populates base_file accordingly
41
+ - [inputs.base] Version-only base image def now also retrieves Checksum via expected .md5 endpoint
42
+
8
43
  ## [2.0.0] - 2024-04-16
9
44
 
10
45
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: offspot-config
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Offspot Config helpers
5
5
  Project-URL: Homepage, https://github.com/offspot/offspot-config
6
6
  Project-URL: Donate, https://www.kiwix.org/en/support-us/
@@ -3,10 +3,13 @@ from __future__ import annotations
3
3
  import re
4
4
  from pathlib import PurePath as Path
5
5
  from typing import Any
6
- from urllib.parse import urlsplit
7
6
 
8
7
  from offspot_config.catalog import app_catalog, get_app_path
9
- from offspot_config.constants import CONTENT_TARGET_PATH
8
+ from offspot_config.constants import (
9
+ CONTENT_TARGET_PATH,
10
+ DATA_PART_PATH,
11
+ INTERNAL_BRANDING_PATH,
12
+ )
10
13
  from offspot_config.inputs.base import BaseConfig
11
14
  from offspot_config.inputs.checksum import Checksum
12
15
  from offspot_config.inputs.file import FileConfig
@@ -14,7 +17,7 @@ from offspot_config.inputs.str import BlockStr
14
17
  from offspot_config.oci_images import OCIImage
15
18
  from offspot_config.packages import AppPackage, FilesPackage, ZimPackage
16
19
  from offspot_config.utils.dashboard import Link, Reader
17
- from offspot_config.utils.download import read_checksum_from
20
+ from offspot_config.utils.misc import b64_encode
18
21
  from offspot_config.utils.sizes import (
19
22
  get_margin_for,
20
23
  get_min_image_size_for,
@@ -36,14 +39,19 @@ METRICS_DATA_PATH = CONTENT_TARGET_PATH / "metrics"
36
39
  KIWIXSERVE_DATA_PATH = CONTENT_TARGET_PATH / "zims"
37
40
  METRICS_VAR_LOG_PATH_HOST = Path("/var/log/metrics")
38
41
  METRICS_VAR_LOG_PATH_CONT = Path("/var/log/host/metrics")
42
+ # hotspot original branding material (usually kept as untouched)
43
+ ORIGINAL_BRANDING_PATH = DATA_PART_PATH / "branding"
44
+ # actual hotspot branding. default from orig, replaced in builder
45
+ BRANDING_PATH = CONTENT_TARGET_PATH / "branding"
46
+
39
47
  KIWIX_ZIM_LOAD_BALANCER_URL = "https://download.kiwix.org/zim/"
40
48
 
41
49
  # data source for “internal images” (out of catalog)
42
50
  INTERNAL_IMAGES = {
43
51
  "captive-portal": {
44
- "source": "ghcr.io/offspot/captive-portal:1.4",
45
- "filesize": 184627200,
46
- "fullsize": 184559684,
52
+ "source": "ghcr.io/offspot/captive-portal:1.4.1",
53
+ "filesize": 189911040,
54
+ "fullsize": 189841976,
47
55
  },
48
56
  "dashboard": {
49
57
  "source": "ghcr.io/offspot/dashboard:1.3.1",
@@ -61,9 +69,9 @@ INTERNAL_IMAGES = {
61
69
  "fullsize": 58922600,
62
70
  },
63
71
  "kiwix-serve": {
64
- "source": "ghcr.io/offspot/kiwix-serve:3.6.0",
65
- "filesize": 62351360,
66
- "fullsize": 62313418,
72
+ "source": "ghcr.io/offspot/kiwix-serve:3.7.0-1",
73
+ "filesize": 58480640,
74
+ "fullsize": 58443713,
67
75
  },
68
76
  "metrics": {
69
77
  "source": "ghcr.io/offspot/metrics:0.3.0",
@@ -71,9 +79,9 @@ INTERNAL_IMAGES = {
71
79
  "fullsize": 167202612,
72
80
  },
73
81
  "reverse-proxy": {
74
- "source": "ghcr.io/offspot/reverse-proxy:1.7",
75
- "filesize": 120350720,
76
- "fullsize": 120279091,
82
+ "source": "ghcr.io/offspot/reverse-proxy:1.8",
83
+ "filesize": 120432640,
84
+ "fullsize": 120368490,
77
85
  },
78
86
  }
79
87
 
@@ -209,23 +217,12 @@ class ConfigBuilder:
209
217
 
210
218
  # Add files for requested readers
211
219
  for reader in self.dashboard_readers:
212
- checksum = None
213
- # download.kiwix.org is known to provide digests via mirrorbrain
214
- if urlsplit(reader.download_url).netloc == "download.kiwix.org":
215
- try:
216
- checksum = Checksum(
217
- algo="md5",
218
- value=read_checksum_from(f"{reader.download_url}.md5"),
219
- )
220
- # we cant assume this this work forever
221
- except Exception:
222
- ...
223
220
  self.add_file(
224
221
  url_or_content=reader.download_url,
225
222
  to=str(KIWIXSERVE_DATA_PATH / reader.filename),
226
223
  via="direct",
227
224
  size=reader.size,
228
- checksum=checksum,
225
+ checksum=reader.checksum,
229
226
  is_url=True,
230
227
  )
231
228
 
@@ -261,6 +258,18 @@ class ConfigBuilder:
261
258
  "target": "/data/zims",
262
259
  "read_only": True,
263
260
  },
261
+ {
262
+ "type": "bind",
263
+ "source": BRANDING_PATH,
264
+ "target": "/var/www/branding",
265
+ "read_only": True,
266
+ },
267
+ {
268
+ "type": "bind",
269
+ "source": ORIGINAL_BRANDING_PATH,
270
+ "target": "/var/www/offspot.branding",
271
+ "read_only": True,
272
+ },
264
273
  ],
265
274
  }
266
275
 
@@ -339,7 +348,19 @@ class ConfigBuilder:
339
348
  "source": str(METRICS_VAR_LOG_PATH_HOST.parent),
340
349
  "target": str(METRICS_VAR_LOG_PATH_CONT),
341
350
  "read_only": False,
342
- }
351
+ },
352
+ {
353
+ "type": "bind",
354
+ "source": BRANDING_PATH,
355
+ "target": "/var/www/branding",
356
+ "read_only": True,
357
+ },
358
+ {
359
+ "type": "bind",
360
+ "source": ORIGINAL_BRANDING_PATH,
361
+ "target": "/var/www/offspot.branding",
362
+ "read_only": True,
363
+ },
343
364
  ],
344
365
  }
345
366
 
@@ -396,7 +417,19 @@ class ConfigBuilder:
396
417
  "source": "/var/run/internet",
397
418
  "target": "/var/run/internet",
398
419
  "read_only": True,
399
- }
420
+ },
421
+ {
422
+ "type": "bind",
423
+ "source": BRANDING_PATH,
424
+ "target": "/src/portal/branding",
425
+ "read_only": True,
426
+ },
427
+ {
428
+ "type": "bind",
429
+ "source": ORIGINAL_BRANDING_PATH,
430
+ "target": "/src/portal/offspot.branding",
431
+ "read_only": True,
432
+ },
400
433
  ],
401
434
  }
402
435
 
@@ -454,6 +487,18 @@ class ConfigBuilder:
454
487
  "target": in_container_packages_path,
455
488
  "read_only": True,
456
489
  },
490
+ {
491
+ "type": "bind",
492
+ "source": BRANDING_PATH,
493
+ "target": "/src/ui/branding",
494
+ "read_only": True,
495
+ },
496
+ {
497
+ "type": "bind",
498
+ "source": ORIGINAL_BRANDING_PATH,
499
+ "target": "/src/ui/offspot.branding",
500
+ "read_only": True,
501
+ },
457
502
  ],
458
503
  }
459
504
 
@@ -481,6 +526,20 @@ class ConfigBuilder:
481
526
  "restart": "unless-stopped",
482
527
  "expose": ["80"],
483
528
  "privileged": True,
529
+ "volumes": [
530
+ {
531
+ "type": "bind",
532
+ "source": BRANDING_PATH,
533
+ "target": "/src/branding",
534
+ "read_only": True,
535
+ },
536
+ {
537
+ "type": "bind",
538
+ "source": ORIGINAL_BRANDING_PATH,
539
+ "target": "/src/offspot.branding",
540
+ "read_only": True,
541
+ },
542
+ ],
484
543
  }
485
544
 
486
545
  self.protected_services.update(
@@ -536,7 +595,19 @@ class ConfigBuilder:
536
595
  "source": f"{CONTENT_TARGET_PATH}/zims",
537
596
  "target": "/data",
538
597
  "read_only": True,
539
- }
598
+ },
599
+ {
600
+ "type": "bind",
601
+ "source": BRANDING_PATH,
602
+ "target": "/data/branding",
603
+ "read_only": True,
604
+ },
605
+ {
606
+ "type": "bind",
607
+ "source": ORIGINAL_BRANDING_PATH,
608
+ "target": "/data/offspot.branding",
609
+ "read_only": True,
610
+ },
540
611
  ],
541
612
  "command": '/bin/sh -c "kiwix-serve --blockexternal '
542
613
  '--port 80 --nodatealiases /data/*.zim"',
@@ -727,7 +798,19 @@ class ConfigBuilder:
727
798
  "source": f"{CONTENT_TARGET_PATH}",
728
799
  "target": "/data",
729
800
  "read_only": True,
730
- }
801
+ },
802
+ {
803
+ "type": "bind",
804
+ "source": BRANDING_PATH,
805
+ "target": "/branding",
806
+ "read_only": True,
807
+ },
808
+ {
809
+ "type": "bind",
810
+ "source": ORIGINAL_BRANDING_PATH,
811
+ "target": "/offspot.branding",
812
+ "read_only": True,
813
+ },
731
814
  ],
732
815
  }
733
816
 
@@ -774,8 +857,11 @@ class ConfigBuilder:
774
857
  if match:
775
858
  repl = self.environ[match.groupdict()["var"]]
776
859
  text = RE_ENVIRON_VAR.sub(repl, text)
777
- resolved = text.replace("${FQDN}", self.fqdn).replace(
778
- "${REVERSE_NAME}", "reverse-proxy"
860
+ resolved = (
861
+ text.replace("${FQDN}", self.fqdn)
862
+ .replace("${REVERSE_NAME}", "reverse-proxy")
863
+ .replace("${BRANDING_PATH}", str(BRANDING_PATH))
864
+ .replace("${ORIGINAL_BRANDING_PATH}", str(ORIGINAL_BRANDING_PATH))
779
865
  )
780
866
  if package:
781
867
  app_dir = get_app_path(package=package)
@@ -809,7 +895,21 @@ class ConfigBuilder:
809
895
  """compute config based on requests"""
810
896
  ...
811
897
 
812
- # add kiwix apps?
898
+ # add original branding so apps can rely on it
899
+ self.ensure_host_path(ORIGINAL_BRANDING_PATH)
900
+ data = b""
901
+ for fpath in INTERNAL_BRANDING_PATH.iterdir():
902
+ data = fpath.read_bytes()
903
+ self.add_file(
904
+ url_or_content=b64_encode(data),
905
+ to=str(ORIGINAL_BRANDING_PATH.joinpath(fpath.name)),
906
+ via="base64",
907
+ size=len(data),
908
+ )
909
+ del data
910
+
911
+ # make sure branding folder exists for mounts
912
+ self.ensure_host_path(BRANDING_PATH)
813
913
 
814
914
  # gen dashboard.yaml
815
915
  if self.with_dashboard:
@@ -65,3 +65,4 @@ POST_EXPANSION_UNWANTED_PATTERNS = (
65
65
  # Windows shortcuts
66
66
  "*.lnk",
67
67
  )
68
+ INTERNAL_BRANDING_PATH = pathlib.Path(__file__).with_name("branding")
@@ -18,11 +18,14 @@ class File:
18
18
  - size: optional[int]
19
19
  size of (expanded) content. If specified, must be >= source file
20
20
  - via: optional[str]
21
- method to process source file (not for content). Values in File.unpack_formats
21
+ method to process source file. Values in File.unpack_formats
22
+ `base64` value only applies to `content`
23
+ other values not applicable if there's a `content` value
22
24
  - url: optional[str]
23
25
  URL to download file from
24
26
  - content: optional[str]
25
27
  plain text content to write to destination
28
+ can be base64 encoded (std) in which case via must be `base64`
26
29
 
27
30
  one of content or url must be supplied. content has priority"""
28
31
 
@@ -31,9 +34,14 @@ class File:
31
34
  unpack_formats: list[str]
32
35
 
33
36
  def __init__(self, payload: dict[str, str | int | dict[str, str]]):
34
- self.unpack_formats = ["direct", *SUPPORTED_UNPACKING_FORMATS]
37
+ self.unpack_formats = ["direct", "base64", *SUPPORTED_UNPACKING_FORMATS]
35
38
  self.url: urllib.parse.ParseResult | None = None
39
+ self.to: pathlib.Path = pathlib.Path(str(payload["to"])).resolve()
40
+ self.via: str = str(payload.get("via", "direct"))
36
41
  self.content: str = str(payload.get("content", "") or "").strip()
42
+ self.checksum: Checksum | None = None
43
+ self._size = -1
44
+ self._fullsize: int | None = None
37
45
 
38
46
  if not self.content:
39
47
  try:
@@ -41,24 +49,19 @@ class File:
41
49
  except Exception as exc:
42
50
  raise ValueError(f"URL “{payload.get('url')}” is incorrect") from exc
43
51
 
44
- self.to: pathlib.Path = pathlib.Path(str(payload["to"])).resolve()
45
52
  if not self.to.is_relative_to(DATA_PART_PATH):
46
53
  raise ValueError(f"{self.to} not a descendent of {DATA_PART_PATH}")
47
54
 
48
- self.via: str = str(payload.get("via", "direct"))
49
55
  if self.via not in self.unpack_formats:
50
56
  raise NotImplementedError(f"Unsupported handler `{self.via}`")
51
57
 
52
58
  # optional checksum
53
- self.checksum = None
54
59
  if "checksum" in payload and isinstance(payload["checksum"], dict):
55
60
  self.checksum = Checksum(**payload["checksum"])
56
61
 
57
- # initialized has unknown
58
- self._size = -1
62
+ # initialized as unknown
59
63
  if "size" in payload and isinstance(payload["size"], (int, str)):
60
64
  self._size: int = int(payload["size"])
61
- self._fullsize: int | None = None
62
65
  if "fullsize" in payload and isinstance(payload["fullsize"], (int, str)):
63
66
  self._fullsize = int(payload["fullsize"])
64
67
 
@@ -104,6 +107,11 @@ class File:
104
107
  """whether a plain text content to be written"""
105
108
  return bool(self.content)
106
109
 
110
+ @property
111
+ def is_base64_encoded(self) -> bool:
112
+ """whether a plain text content to be written"""
113
+ return self.via == "base64"
114
+
107
115
  @property
108
116
  def is_local(self) -> bool:
109
117
  """whether referencing a local file"""
@@ -124,7 +132,9 @@ class File:
124
132
  msg += f", url={self.geturl()}"
125
133
  if self.content:
126
134
  msg += f", content={self.content.splitlines()[0][:10]}"
127
- msg += f", size={self.size})"
135
+ msg += f", size={self.size}"
136
+ msg += f", checksum={self.checksum.as_aria if self.checksum else None}"
137
+ msg += ")"
128
138
  return msg
129
139
 
130
140
  def __str__(self) -> str:
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from attrs import define
6
+ from typeguard import typechecked
7
+
8
+ from offspot_config.constants import DATA_PART_PATH
9
+ from offspot_config.file import File
10
+ from offspot_config.inputs.checksum import Checksum
11
+ from offspot_config.utils.download import read_checksum_from
12
+ from offspot_config.utils.misc import parse_size
13
+
14
+
15
+ def get_base_from(base: BaseConfig) -> File:
16
+ """Infer url from flexible `base` and return a File"""
17
+ payload: dict[str, str | int | dict[str, str]] = {
18
+ "url": str(base.source),
19
+ "to": str(DATA_PART_PATH / "-"),
20
+ }
21
+ match = re.match(
22
+ r"^(?P<version>\d\.\d\.\d)(?P<extra>[a-z0-9\-\.\_]*)", str(base.source)
23
+ )
24
+ if match:
25
+ version = "".join(match.groups())
26
+ payload["url"] = (
27
+ f"https://drive.offspot.it/base/offspot-base-arm64-{version}.img"
28
+ )
29
+ try:
30
+ payload["checksum"] = Checksum(
31
+ algo="md5", value=read_checksum_from(f"{payload['url']}.md5")
32
+ ).to_dict()
33
+ except Exception:
34
+ ...
35
+ elif isinstance(base.checksum, Checksum):
36
+ payload["checksum"] = base.checksum.to_dict()
37
+ return File(payload)
38
+
39
+
40
+ @typechecked
41
+ @define(kw_only=True)
42
+ class BaseConfig:
43
+ source: str | File
44
+ rootfs_size: str | int
45
+ checksum: dict[str, str] | Checksum | None = None
46
+
47
+ def __attrs_post_init__(self):
48
+ if self.rootfs_size and isinstance(self.rootfs_size, str):
49
+ self.rootfs_size = parse_size(self.rootfs_size)
50
+ if self.checksum and isinstance(self.checksum, dict):
51
+ self.checksum = Checksum(**self.checksum)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from attrs import define
3
+ from attrs import asdict, define
4
4
  from typeguard import typechecked
5
5
 
6
6
  from offspot_config.constants import SUPPORTED_CHECKSUM_ALGORITHMS
@@ -47,3 +47,6 @@ class Checksum:
47
47
  def as_aria(self) -> str:
48
48
  """aria2-compatible checksum format: {algo}={digest}"""
49
49
  return f"{self.algo}={self.digest}"
50
+
51
+ def to_dict(self) -> dict[str, str]:
52
+ return asdict(self)
@@ -35,7 +35,7 @@ class MainConfig:
35
35
  self.base = BaseConfig(**self.base)
36
36
 
37
37
  if isinstance(self.base.source, str):
38
- self.base_file = get_base_from(self.base.source)
38
+ self.base_file = get_base_from(self.base)
39
39
 
40
40
  if isinstance(self.output, dict):
41
41
  self.output = OutputConfig(**self.output)
@@ -0,0 +1 @@
1
+ WAYS = ("direct", "base64", "bztar", "gztar", "tar", "xztar", "zip")
@@ -4,6 +4,7 @@ import pathlib
4
4
  import urllib.parse
5
5
  from typing import NamedTuple, TypeVar
6
6
 
7
+ from offspot_config.inputs.checksum import Checksum
7
8
  from offspot_config.utils.download import get_online_rsc_size
8
9
 
9
10
  R = TypeVar("R", bound="Reader")
@@ -19,9 +20,15 @@ class Reader(NamedTuple):
19
20
  download_url: str
20
21
  filename: str
21
22
  size: int
23
+ checksum: Checksum | None = None
22
24
 
23
25
  def to_dict(self) -> dict[str, str | int]:
24
- return {field: getattr(self, field) for field in self._fields}
26
+ def value_or_dict(value):
27
+ if hasattr(value, "to_dict"):
28
+ return value.to_dict()
29
+ return value
30
+
31
+ return {field: value_or_dict(getattr(self, field)) for field in self._fields}
25
32
 
26
33
  @property
27
34
  def order(self) -> int:
@@ -31,13 +38,16 @@ class Reader(NamedTuple):
31
38
  )
32
39
 
33
40
  @classmethod
34
- def using(cls: type[R], platform: str, download_url: str) -> R:
41
+ def using(
42
+ cls: type[R], platform: str, download_url: str, checksum: Checksum | None = None
43
+ ) -> R:
35
44
  """Reader from a platform name and download URL"""
36
45
  return cls(
37
46
  platform=platform,
38
47
  download_url=download_url,
39
48
  size=get_online_rsc_size(download_url),
40
49
  filename=cls.filename_from_url(download_url),
50
+ checksum=checksum,
41
51
  )
42
52
 
43
53
  @staticmethod
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import datetime
4
5
  import fnmatch
5
6
  import inspect
@@ -142,6 +143,16 @@ def expand_file(src: pathlib.Path, method: str, dest: pathlib.Path):
142
143
  fpath.unlink(missing_ok=True)
143
144
 
144
145
 
146
+ def b64_encode(data: bytes) -> str:
147
+ """ASCII based64 repr of data"""
148
+ return base64.standard_b64encode(data).decode("ASCII")
149
+
150
+
151
+ def b64_decode(data: str) -> bytes:
152
+ """ASCII based64 repr of data"""
153
+ return base64.standard_b64decode(data.encode("ASCII"))
154
+
155
+
145
156
  class SimpleAttrs:
146
157
  """dict-like xattr wrapper to save specifying user. prefix"""
147
158
 
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0"
@@ -1,7 +1,7 @@
1
1
  import pytest # pyright: ignore [reportMissingImports]
2
2
 
3
3
 
4
- @pytest.fixture
4
+ @pytest.fixture(scope="session")
5
5
  def mini_config_yaml():
6
6
  yield """
7
7
  ---
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest # pyright: ignore [reportMissingImports]
4
+
5
+ from offspot_config.inputs.checksum import Checksum
6
+ from offspot_config.inputs.mainconfig import MainConfig
7
+
8
+
9
+ def test_main_config(mini_config_yaml: str):
10
+ main_config = MainConfig.read_from(mini_config_yaml)
11
+ assert main_config
12
+ assert len(main_config.all_files) == 3
13
+ assert len(main_config.all_images) == 1
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "config, md5sum",
18
+ [
19
+ (
20
+ """
21
+ ---
22
+ base:
23
+ source: http://yolo
24
+ rootfs_size: 2638217216
25
+ checksum:
26
+ algo: md5
27
+ value: 747fd0841b56d2d5158e0e65646b1be1
28
+ kind: digest
29
+ output:
30
+ size: auto
31
+ """,
32
+ "747fd0841b56d2d5158e0e65646b1be1",
33
+ ),
34
+ (
35
+ """
36
+ ---
37
+ base:
38
+ source: http://yolo
39
+ rootfs_size: 2638217216
40
+ output:
41
+ size: auto
42
+ """,
43
+ None,
44
+ ),
45
+ (
46
+ """
47
+ ---
48
+ base:
49
+ source: 1.2.1
50
+ rootfs_size: 2638217216
51
+ output:
52
+ size: auto
53
+ """,
54
+ "bb6fdcee36678ffcc88431dd502dfc11",
55
+ ),
56
+ (
57
+ """
58
+ ---
59
+ base:
60
+ source: 1.2.1
61
+ rootfs_size: 2638217216
62
+ checksum:
63
+ algo: md5
64
+ value: YOLO
65
+ kind: digest
66
+ output:
67
+ size: auto
68
+ """,
69
+ "bb6fdcee36678ffcc88431dd502dfc11",
70
+ ),
71
+ (
72
+ """
73
+ ---
74
+ base:
75
+ source: 1.0.0
76
+ rootfs_size: 2638217216
77
+ output:
78
+ size: auto
79
+ """,
80
+ None,
81
+ ),
82
+ ],
83
+ )
84
+ def test_base_config(config: str, md5sum: str | None):
85
+ main_config = MainConfig.read_from(config)
86
+ assert main_config
87
+ assert len(main_config.all_files) == 0
88
+ assert len(main_config.all_images) == 0
89
+ assert main_config.base
90
+ assert main_config.base_file
91
+ checksum = Checksum(algo="md5", value=md5sum) if md5sum else None
92
+ assert main_config.base_file.checksum == checksum
@@ -1,5 +1,6 @@
1
1
  import pytest # pyright: ignore [reportMissingImports]
2
2
 
3
+ from offspot_config.inputs.checksum import Checksum
3
4
  from offspot_config.utils.dashboard import Reader
4
5
 
5
6
  REALISTIC_VALUES = [
@@ -8,24 +9,28 @@ REALISTIC_VALUES = [
8
9
  "https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_x86_64_2.3.1-4.appimage",
9
10
  "kiwix-desktop_x86_64_2.3.1-4.appimage",
10
11
  146629824,
12
+ "899279fb76e357afe33bbdd968750376",
11
13
  ),
12
14
  (
13
15
  "android",
14
16
  "https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
15
17
  "kiwix-3.9.1.apk",
16
18
  79629012,
19
+ "d2ede8e23b4095718c508f44a341f687",
17
20
  ),
18
21
  (
19
22
  "windows",
20
23
  "https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
21
24
  "kiwix-desktop_windows_x64_2.3.1-2.zip",
22
25
  126628211,
26
+ "adf9b64b5c6906427d7a1c8bdfc31546",
23
27
  ),
24
28
  (
25
29
  "macos",
26
30
  "https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
27
31
  "kiwix-desktop-macos_3.1.0.dmg",
28
32
  16051402,
33
+ "747fd0841b56d2d5158e0e65646b1be1",
29
34
  ),
30
35
  ]
31
36
 
@@ -56,34 +61,73 @@ def test_filename_from_url(url: str, expected_filename: str):
56
61
  assert Reader.filename_from_url(url) == expected_filename
57
62
 
58
63
 
64
+ def get_checksum_from(md5sum: str) -> Checksum:
65
+ return Checksum(algo="md5", value=md5sum)
66
+
67
+
59
68
  def test_reader_is_tuple():
60
69
  platform = "windows"
61
70
  url = "http://some.tld/file"
62
71
  filename = "one"
63
72
  size = 4
64
- assert Reader(platform, url, filename, size) == (platform, url, filename, size)
73
+ checksum = None
74
+ assert Reader(platform, url, filename, size, checksum) == (
75
+ platform,
76
+ url,
77
+ filename,
78
+ size,
79
+ checksum,
80
+ )
65
81
  assert Reader(
66
- download_url=url, size=size, platform=platform, filename=filename
67
- ) == (platform, url, filename, size)
68
- assert isinstance(Reader(platform, url, filename, size), Reader)
69
- assert isinstance(Reader(platform, url, filename, size), tuple)
70
- tuple_ = (platform, url, filename, size)
82
+ download_url=url,
83
+ size=size,
84
+ platform=platform,
85
+ filename=filename,
86
+ checksum=checksum,
87
+ ) == (platform, url, filename, size, checksum)
88
+ assert isinstance(Reader(platform, url, filename, size, checksum), Reader)
89
+ assert isinstance(Reader(platform, url, filename, size, checksum), tuple)
90
+ tuple_ = (platform, url, filename, size, checksum)
71
91
  casted = Reader._make(tuple_)
72
92
  assert isinstance(casted, Reader)
73
93
 
74
94
 
75
- @pytest.mark.parametrize("platform, download_url, filename, size", REALISTIC_VALUES)
76
- def test_reader_using(platform: str, download_url: str, filename: str, size: int):
77
- assert Reader.using(platform, download_url) == Reader(
78
- platform, download_url, filename, size
95
+ @pytest.mark.parametrize(
96
+ "platform, download_url, filename, size, md5sum", REALISTIC_VALUES
97
+ )
98
+ def test_reader_using(
99
+ platform: str, download_url: str, filename: str, size: int, md5sum: str
100
+ ):
101
+ assert Reader.using(
102
+ platform, download_url, checksum=get_checksum_from(md5sum)
103
+ ) == Reader(
104
+ platform,
105
+ download_url,
106
+ filename,
107
+ size,
108
+ get_checksum_from(md5sum),
109
+ )
110
+ assert Reader.using(platform=platform, download_url=download_url) == Reader(
111
+ platform,
112
+ download_url,
113
+ filename,
114
+ size,
115
+ None,
79
116
  )
80
117
  assert Reader.using(platform=platform, download_url=download_url) == Reader(
81
- platform, download_url, filename, size
118
+ platform,
119
+ download_url,
120
+ filename,
121
+ size,
82
122
  )
83
123
 
84
124
 
85
- @pytest.mark.parametrize("platform, download_url, filename, size", REALISTIC_VALUES)
86
- def test_invalid_data(platform: str, download_url: str, filename: str, size: int):
125
+ @pytest.mark.parametrize(
126
+ "platform, download_url, filename, size, md5sum", REALISTIC_VALUES
127
+ )
128
+ def test_invalid_data(
129
+ platform: str, download_url: str, filename: str, size: int, md5sum: str
130
+ ):
87
131
  with pytest.raises(TypeError):
88
132
  Reader(platform, download_url, filename) # pyright: ignore [reportCallIssue]
89
133
  with pytest.raises(TypeError):
@@ -92,6 +136,7 @@ def test_invalid_data(platform: str, download_url: str, filename: str, size: int
92
136
  download_url,
93
137
  filename,
94
138
  size,
139
+ get_checksum_from(md5sum),
95
140
  32, # pyright: ignore [reportCallIssue]
96
141
  )
97
142
  with pytest.raises(TypeError):
@@ -105,7 +150,10 @@ def test_invalid_data(platform: str, download_url: str, filename: str, size: int
105
150
 
106
151
 
107
152
  def test_sort_order():
108
- readers = [Reader(*values) for values in REALISTIC_VALUES]
153
+ readers = [
154
+ Reader(*values[:-1], checksum=get_checksum_from(md5sum=values[-1]))
155
+ for values in REALISTIC_VALUES
156
+ ]
109
157
  sorted_readers = sorted(readers, key=Reader.sort)
110
158
  assert readers != sorted_readers
111
159
  assert [r.platform for r in sorted_readers] == [
@@ -117,18 +165,20 @@ def test_sort_order():
117
165
 
118
166
 
119
167
  @pytest.mark.parametrize(
120
- "platform, download_url, filename, size, expected_dict",
168
+ "platform, download_url, filename, size, md5sum, expected_dict",
121
169
  [
122
170
  (
123
171
  "android",
124
172
  "https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
125
173
  "kiwix-3.9.1.apk",
126
174
  79629012,
175
+ None,
127
176
  {
128
177
  "platform": "android",
129
178
  "download_url": "https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
130
179
  "filename": "kiwix-3.9.1.apk",
131
180
  "size": 79629012,
181
+ "checksum": None,
132
182
  },
133
183
  ),
134
184
  (
@@ -136,11 +186,17 @@ def test_sort_order():
136
186
  "https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
137
187
  "kiwix-desktop_windows_x64_2.3.1-2.zip",
138
188
  126628211,
189
+ "adf9b64b5c6906427d7a1c8bdfc31546",
139
190
  {
140
191
  "platform": "windows",
141
192
  "download_url": "https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
142
193
  "filename": "kiwix-desktop_windows_x64_2.3.1-2.zip",
143
194
  "size": 126628211,
195
+ "checksum": {
196
+ "algo": "md5",
197
+ "value": "adf9b64b5c6906427d7a1c8bdfc31546",
198
+ "kind": "digest",
199
+ },
144
200
  },
145
201
  ),
146
202
  (
@@ -148,18 +204,23 @@ def test_sort_order():
148
204
  "https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
149
205
  None,
150
206
  None,
207
+ None,
151
208
  {
152
209
  "platform": "macos",
153
210
  "download_url": "https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
154
211
  "filename": "kiwix-desktop-macos_3.1.0.dmg",
155
212
  "size": 16051402,
213
+ "checksum": None,
156
214
  },
157
215
  ),
158
216
  ],
159
217
  )
160
- def test_to_dict(platform, download_url, filename, size, expected_dict):
218
+ def test_to_dict(platform, download_url, filename, size, md5sum, expected_dict):
219
+ checksum = get_checksum_from(md5sum) if md5sum else None
161
220
  if filename and size:
162
- reader = Reader(platform, download_url, filename, size)
221
+ reader = Reader(platform, download_url, filename, size, checksum)
163
222
  else:
164
- reader = Reader.using(platform=platform, download_url=download_url)
223
+ reader = Reader.using(
224
+ platform=platform, download_url=download_url, checksum=checksum
225
+ )
165
226
  assert reader.to_dict() == expected_dict
@@ -1,30 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
-
5
- from attrs import define
6
- from typeguard import typechecked
7
-
8
- from offspot_config.constants import DATA_PART_PATH
9
- from offspot_config.file import File
10
- from offspot_config.utils.misc import parse_size
11
-
12
-
13
- def get_base_from(url: str) -> File:
14
- """Infer url from flexible `base` and return a File"""
15
- match = re.match(r"^(?P<version>\d\.\d\.\d)(?P<extra>[a-z0-9\-\.\_]*)", url)
16
- if match:
17
- version = "".join(match.groups())
18
- url = f"https://drive.offspot.it/base/offspot-base-arm64-{version}.img.xz"
19
- return File({"url": url, "to": str(DATA_PART_PATH / "-")})
20
-
21
-
22
- @typechecked
23
- @define(kw_only=True)
24
- class BaseConfig:
25
- source: str | File
26
- rootfs_size: str | int
27
-
28
- def __attrs_post_init__(self):
29
- if self.rootfs_size and isinstance(self.rootfs_size, str):
30
- self.rootfs_size = parse_size(self.rootfs_size)
@@ -1 +0,0 @@
1
- WAYS = ("direct", "bztar", "gztar", "tar", "xztar", "zip")
@@ -1 +0,0 @@
1
- __version__ = "2.0.0"
@@ -1,8 +0,0 @@
1
- from offspot_config.inputs.mainconfig import MainConfig
2
-
3
-
4
- def test_main_config(mini_config_yaml: str):
5
- main_config = MainConfig.read_from(mini_config_yaml)
6
- assert main_config
7
- assert len(main_config.all_files) == 3
8
- assert len(main_config.all_images) == 1
File without changes
File without changes
File without changes