divejson 0.4.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 (139) hide show
  1. divejson-0.4.0/.github/scripts/release_bump.py +276 -0
  2. divejson-0.4.0/.github/workflows/ci.yml +119 -0
  3. divejson-0.4.0/.github/workflows/pr-title.yml +65 -0
  4. divejson-0.4.0/.github/workflows/release-bump.yml +281 -0
  5. divejson-0.4.0/.github/workflows/release.yml +65 -0
  6. divejson-0.4.0/.gitignore +9 -0
  7. divejson-0.4.0/CHANGELOG.md +243 -0
  8. divejson-0.4.0/CONTRIBUTING.md +181 -0
  9. divejson-0.4.0/LICENSE +21 -0
  10. divejson-0.4.0/PKG-INFO +293 -0
  11. divejson-0.4.0/README.md +274 -0
  12. divejson-0.4.0/SPEC_REF +1 -0
  13. divejson-0.4.0/divejson/__init__.py +120 -0
  14. divejson-0.4.0/divejson/cli.py +414 -0
  15. divejson-0.4.0/divejson/conform.py +544 -0
  16. divejson-0.4.0/divejson/converter.py +600 -0
  17. divejson-0.4.0/divejson/fit.py +1319 -0
  18. divejson-0.4.0/divejson/py.typed +0 -0
  19. divejson-0.4.0/divejson/registry.py +474 -0
  20. divejson-0.4.0/divejson/series.py +195 -0
  21. divejson-0.4.0/divejson/ssrf.py +698 -0
  22. divejson-0.4.0/divejson/suunto_json.py +1199 -0
  23. divejson-0.4.0/divejson/suunto_xml.py +1003 -0
  24. divejson-0.4.0/divejson/uddf.py +1163 -0
  25. divejson-0.4.0/divejson/uddf_write.py +1127 -0
  26. divejson-0.4.0/divejson/validate.py +410 -0
  27. divejson-0.4.0/divejson/xmlsource.py +201 -0
  28. divejson-0.4.0/docs/converting.md +334 -0
  29. divejson-0.4.0/docs/fit-mapping.md +485 -0
  30. divejson-0.4.0/docs/ssrf-mapping.md +336 -0
  31. divejson-0.4.0/docs/suunto-json-mapping.md +492 -0
  32. divejson-0.4.0/docs/suunto-xml-mapping.md +495 -0
  33. divejson-0.4.0/docs/uddf-mapping.md +365 -0
  34. divejson-0.4.0/docs/uddf-writing.md +381 -0
  35. divejson-0.4.0/fixtures/README.md +147 -0
  36. divejson-0.4.0/fixtures/fit/suunto-d5.divejson +850 -0
  37. divejson-0.4.0/fixtures/fit/suunto-d5.fit +0 -0
  38. divejson-0.4.0/fixtures/fit/suunto-ocean.divejson +9508 -0
  39. divejson-0.4.0/fixtures/fit/suunto-ocean.fit +0 -0
  40. divejson-0.4.0/fixtures/invalid/agency-other-missing.divejson +459 -0
  41. divejson-0.4.0/fixtures/invalid/avg-depth-exceeds-max.divejson +460 -0
  42. divejson-0.4.0/fixtures/invalid/bad-version.divejson +460 -0
  43. divejson-0.4.0/fixtures/invalid/bbox-missing-corner.divejson +459 -0
  44. divejson-0.4.0/fixtures/invalid/bbox-south-exceeds-north.divejson +460 -0
  45. divejson-0.4.0/fixtures/invalid/bbox-without-position.divejson +456 -0
  46. divejson-0.4.0/fixtures/invalid/channel-length-mismatch.divejson +461 -0
  47. divejson-0.4.0/fixtures/invalid/course-dates-reversed.divejson +460 -0
  48. divejson-0.4.0/fixtures/invalid/dangling-reference.divejson +461 -0
  49. divejson-0.4.0/fixtures/invalid/duplicate-json-member.divejson +6 -0
  50. divejson-0.4.0/fixtures/invalid/duplicate-uuid.divejson +460 -0
  51. divejson-0.4.0/fixtures/invalid/event-other-without-label.divejson +459 -0
  52. divejson-0.4.0/fixtures/invalid/missing-format.divejson +459 -0
  53. divejson-0.4.0/fixtures/invalid/naive-exported-at.divejson +460 -0
  54. divejson-0.4.0/fixtures/invalid/non-increasing-samples.divejson +460 -0
  55. divejson-0.4.0/fixtures/invalid/null-member.divejson +460 -0
  56. divejson-0.4.0/fixtures/invalid/oxygen-helium-sum.divejson +460 -0
  57. divejson-0.4.0/fixtures/invalid/position-incomplete.divejson +459 -0
  58. divejson-0.4.0/fixtures/invalid/pressure-order.divejson +460 -0
  59. divejson-0.4.0/fixtures/invalid/profile-duration-short.divejson +460 -0
  60. divejson-0.4.0/fixtures/invalid/species-no-identity.divejson +460 -0
  61. divejson-0.4.0/fixtures/invalid/trailing-newline-datetime.divejson +460 -0
  62. divejson-0.4.0/fixtures/invalid/trip-dates-reversed.divejson +460 -0
  63. divejson-0.4.0/fixtures/invalid/undefined-member.divejson +461 -0
  64. divejson-0.4.0/fixtures/invalid/version-before-format.divejson +5 -0
  65. divejson-0.4.0/fixtures/invalid/version-not-second.divejson +5 -0
  66. divejson-0.4.0/fixtures/ssrf/refusals.divejson +94 -0
  67. divejson-0.4.0/fixtures/ssrf/refusals.ssrf +39 -0
  68. divejson-0.4.0/fixtures/ssrf/subsurface.divejson +131 -0
  69. divejson-0.4.0/fixtures/ssrf/subsurface.ssrf +42 -0
  70. divejson-0.4.0/fixtures/ssrf/trip-grouping.divejson +80 -0
  71. divejson-0.4.0/fixtures/ssrf/trip-grouping.ssrf +33 -0
  72. divejson-0.4.0/fixtures/suunto_json/header-only.divejson +27 -0
  73. divejson-0.4.0/fixtures/suunto_json/header-only.json +15 -0
  74. divejson-0.4.0/fixtures/suunto_json/not-a-dive.divejson +18 -0
  75. divejson-0.4.0/fixtures/suunto_json/not-a-dive.json +17 -0
  76. divejson-0.4.0/fixtures/suunto_json/purged-regulator.divejson +73 -0
  77. divejson-0.4.0/fixtures/suunto_json/purged-regulator.json +50 -0
  78. divejson-0.4.0/fixtures/suunto_json/suunto-d5.divejson +123 -0
  79. divejson-0.4.0/fixtures/suunto_json/suunto-d5.json +76 -0
  80. divejson-0.4.0/fixtures/suunto_json/suunto-ocean.divejson +114 -0
  81. divejson-0.4.0/fixtures/suunto_json/suunto-ocean.json +111 -0
  82. divejson-0.4.0/fixtures/suunto_xml/freedive.divejson +18 -0
  83. divejson-0.4.0/fixtures/suunto_xml/freedive.xml +128 -0
  84. divejson-0.4.0/fixtures/suunto_xml/nitrox-deco.divejson +145 -0
  85. divejson-0.4.0/fixtures/suunto_xml/nitrox-deco.xml +198 -0
  86. divejson-0.4.0/fixtures/suunto_xml/refusals.divejson +80 -0
  87. divejson-0.4.0/fixtures/suunto_xml/refusals.xml +154 -0
  88. divejson-0.4.0/fixtures/suunto_xml/suunto-d5.divejson +92 -0
  89. divejson-0.4.0/fixtures/suunto_xml/suunto-d5.xml +169 -0
  90. divejson-0.4.0/fixtures/suunto_xml/unlabelled-pressure.divejson +97 -0
  91. divejson-0.4.0/fixtures/suunto_xml/unlabelled-pressure.xml +119 -0
  92. divejson-0.4.0/fixtures/uddf/divelogs.divejson +95 -0
  93. divejson-0.4.0/fixtures/uddf/divelogs.uddf +112 -0
  94. divejson-0.4.0/fixtures/uddf/legacy-writer.divejson +64 -0
  95. divejson-0.4.0/fixtures/uddf/legacy-writer.uddf +66 -0
  96. divejson-0.4.0/fixtures/uddf/mix-only-cylinder.divejson +67 -0
  97. divejson-0.4.0/fixtures/uddf/mix-only-cylinder.uddf +70 -0
  98. divejson-0.4.0/fixtures/uddf/opendiving.divejson +197 -0
  99. divejson-0.4.0/fixtures/uddf/opendiving.uddf +161 -0
  100. divejson-0.4.0/fixtures/uddf/subsurface.divejson +129 -0
  101. divejson-0.4.0/fixtures/uddf/subsurface.uddf +163 -0
  102. divejson-0.4.0/fixtures/valid/demo-logbook.divejson +4164 -0
  103. divejson-0.4.0/fixtures/valid/minimal.divejson +5 -0
  104. divejson-0.4.0/fixtures/valid/technical-dive.divejson +465 -0
  105. divejson-0.4.0/fixtures/write/uddf/opendiving.divejson +197 -0
  106. divejson-0.4.0/fixtures/write/uddf/opendiving.uddf +161 -0
  107. divejson-0.4.0/fixtures/write/uddf/technical-dive.divejson +465 -0
  108. divejson-0.4.0/fixtures/write/uddf/technical-dive.uddf +250 -0
  109. divejson-0.4.0/pyproject.toml +67 -0
  110. divejson-0.4.0/schema/1.0/divejson.schema.json +450 -0
  111. divejson-0.4.0/tests/fitbuild.py +312 -0
  112. divejson-0.4.0/tests/fixtures/README.md +33 -0
  113. divejson-0.4.0/tests/fixtures/uddf_3.2.2.xsd +1888 -0
  114. divejson-0.4.0/tests/helpers.py +179 -0
  115. divejson-0.4.0/tests/test_cli.py +274 -0
  116. divejson-0.4.0/tests/test_conform.py +427 -0
  117. divejson-0.4.0/tests/test_converter.py +187 -0
  118. divejson-0.4.0/tests/test_fit_fixtures.py +203 -0
  119. divejson-0.4.0/tests/test_fit_parsing.py +838 -0
  120. divejson-0.4.0/tests/test_fit_units.py +293 -0
  121. divejson-0.4.0/tests/test_package.py +89 -0
  122. divejson-0.4.0/tests/test_registry.py +462 -0
  123. divejson-0.4.0/tests/test_release_bump.py +339 -0
  124. divejson-0.4.0/tests/test_series.py +205 -0
  125. divejson-0.4.0/tests/test_ssrf_fixtures.py +205 -0
  126. divejson-0.4.0/tests/test_ssrf_parsing.py +380 -0
  127. divejson-0.4.0/tests/test_ssrf_units.py +198 -0
  128. divejson-0.4.0/tests/test_suunto_json_fixtures.py +326 -0
  129. divejson-0.4.0/tests/test_suunto_json_parsing.py +571 -0
  130. divejson-0.4.0/tests/test_suunto_json_units.py +235 -0
  131. divejson-0.4.0/tests/test_suunto_xml_fixtures.py +131 -0
  132. divejson-0.4.0/tests/test_suunto_xml_parsing.py +628 -0
  133. divejson-0.4.0/tests/test_suunto_xml_units.py +230 -0
  134. divejson-0.4.0/tests/test_uddf_fixtures.py +125 -0
  135. divejson-0.4.0/tests/test_uddf_parsing.py +528 -0
  136. divejson-0.4.0/tests/test_uddf_units.py +177 -0
  137. divejson-0.4.0/tests/test_uddf_write_fixtures.py +344 -0
  138. divejson-0.4.0/tests/test_uddf_writing.py +524 -0
  139. divejson-0.4.0/tests/test_xmlsource.py +123 -0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env python3
2
+ """Work out what version a release is, and write it into the two files that carry it.
3
+
4
+ `.github/workflows/release-bump.yml` runs this over a checkout of `main`; the two
5
+ rewrites then land through a pull request and the tag goes on the commit that lands. It
6
+ is a script beside the workflow rather than part of the package, because nothing a `pip
7
+ install divejson` gets has any business knowing how this repository cuts a release.
8
+
9
+ The version is computed from the conventional-commit subjects since the last `v*` tag,
10
+ which — the repository being squash-only with `PR_TITLE` as the subject — are the titles
11
+ of the pull requests in the window. **Below 1.0 a breaking change moves the minor**: 0.x
12
+ has no major to spend, and the promise 0.x makes is precisely that a minor may break.
13
+ From 1.0.0 the rule is ordinary semver.
14
+
15
+ Everything it will not do is a refusal rather than a guess, because every one of them is
16
+ a release that is wrong in a way nobody would notice until PyPI had it:
17
+
18
+ - an empty `## Unreleased` section publishes a version whose notes say nothing;
19
+ - a `__version__` that disagrees with the newest tag means a bump landed and its tag
20
+ never did, where the answer is to tag what is already there rather than to bump past
21
+ it, silently burning a version number;
22
+ - a version that is not ahead of the current one, or one whose tag already exists, is a
23
+ release PyPI would refuse after `main` had already moved.
24
+
25
+ Run it by hand to see what a release would be — it only touches the working tree, and
26
+ `git diff` is the whole report:
27
+
28
+ python3 .github/scripts/release_bump.py
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import os
35
+ import re
36
+ import subprocess
37
+ import sys
38
+ from collections.abc import Sequence
39
+ from pathlib import Path
40
+
41
+ # The line hatchling reads the built version out of (`[tool.hatch.version]`), matched
42
+ # whole so that a `__version__` mentioned in a docstring or a comment cannot be the one
43
+ # that moves. `[^"\n]` and not `[^"]`: a class that admits a newline would match a
44
+ # `__version__ = "0.3.0` whose closing quote is on the next line, which is the shape a
45
+ # version string carrying a newline writes and the shape the read-back below has to catch.
46
+ VERSION_LINE = re.compile(r'^__version__ = "(?P<version>[^"\n]*)"$', re.MULTILINE)
47
+
48
+ # Three plain integers and nothing else. A pre-release or build-metadata suffix would
49
+ # make `bump` ambiguous and `release.yml`'s `dist/divejson-$version.tar.gz` check depend
50
+ # on how the build normalises it, so it is refused rather than half-supported.
51
+ #
52
+ # Used with `fullmatch` rather than anchored with `$`, because Python's `$` also matches
53
+ # *before* a trailing newline: `"0.3.0\n"` would pass an anchored `match`, be written into
54
+ # the version line as two physical lines, and leave a package that does not import.
55
+ SEMVER = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)")
56
+
57
+ # The same shape `.github/workflows/pr-title.yml` enforces on every title that becomes a
58
+ # subject here, with the type left open: that workflow owns the list of types, and a
59
+ # second copy of it in this file is a second place for it to go stale. What this needs
60
+ # from a subject is only whether it carries `!`, and whether its type is `feat` or `fix`.
61
+ SUBJECT = re.compile(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]*)\))?(?P<breaking>!)?: ")
62
+
63
+ UNRELEASED = "## Unreleased"
64
+
65
+ INIT_PATH = Path("divejson") / "__init__.py"
66
+ CHANGELOG_PATH = Path("CHANGELOG.md")
67
+
68
+
69
+ class ReleaseError(Exception):
70
+ """Something about the tree or the request means there is no release to cut."""
71
+
72
+
73
+ def parse_version(text: str) -> tuple[int, int, int]:
74
+ match = SEMVER.fullmatch(text)
75
+ if match is None:
76
+ raise ReleaseError(f"{text!r} is not a major.minor.patch version")
77
+ return int(match["major"]), int(match["minor"]), int(match["patch"])
78
+
79
+
80
+ def read_version(init_text: str) -> str:
81
+ """The version the package currently declares."""
82
+ found = VERSION_LINE.findall(init_text)
83
+ if len(found) != 1:
84
+ raise ReleaseError(f"{INIT_PATH} declares __version__ {len(found)} times, not once")
85
+ return found[0]
86
+
87
+
88
+ def bump_kind(subjects: Sequence[str]) -> str:
89
+ """What the window as a whole asks for: `breaking`, `feature`, `fix` or `none`.
90
+
91
+ Subjects that are not conventional subjects contribute nothing. They exist — the
92
+ handful of commits that predate `pr-title.yml` — and one of them is not evidence
93
+ either way.
94
+ """
95
+ types: set[str] = set()
96
+ for subject in subjects:
97
+ match = SUBJECT.match(subject)
98
+ if match is None:
99
+ continue
100
+ if match["breaking"]:
101
+ return "breaking"
102
+ types.add(match["type"])
103
+ if "feat" in types:
104
+ return "feature"
105
+ if "fix" in types:
106
+ return "fix"
107
+ return "none"
108
+
109
+
110
+ def next_version(current: str, subjects: Sequence[str]) -> str:
111
+ major, minor, patch = parse_version(current)
112
+ kind = bump_kind(subjects)
113
+ if major == 0:
114
+ # Below 1.0 a break has no major to move, and a minor is what 0.x already warns
115
+ # about — so breaking and feature land on the same answer, and only a window of
116
+ # fixes (or of changes that claim nothing at all) is a patch.
117
+ if kind in ("breaking", "feature"):
118
+ return f"0.{minor + 1}.0"
119
+ return f"0.{minor}.{patch + 1}"
120
+ if kind == "breaking":
121
+ return f"{major + 1}.0.0"
122
+ if kind == "feature":
123
+ return f"{major}.{minor + 1}.0"
124
+ return f"{major}.{minor}.{patch + 1}"
125
+
126
+
127
+ def rewrite_version(init_text: str, version: str) -> str:
128
+ read_version(init_text) # exactly one line to move, or nothing is rewritten
129
+ return VERSION_LINE.sub(f'__version__ = "{version}"', init_text, count=1)
130
+
131
+
132
+ def rewrite_changelog(changelog_text: str, version: str) -> str:
133
+ """`## Unreleased` becomes `## <version>`, with a fresh empty `## Unreleased` above."""
134
+ lines = changelog_text.split("\n")
135
+ headings = [i for i, line in enumerate(lines) if line.rstrip() == UNRELEASED]
136
+ if len(headings) != 1:
137
+ raise ReleaseError(
138
+ f"{CHANGELOG_PATH} has {len(headings)} `{UNRELEASED}` headings, not one"
139
+ )
140
+ start = headings[0]
141
+ end = next(
142
+ (i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")),
143
+ len(lines),
144
+ )
145
+ if not "\n".join(lines[start + 1 : end]).strip():
146
+ raise ReleaseError(
147
+ f"the `{UNRELEASED}` section of {CHANGELOG_PATH} is empty — a release with no "
148
+ "notes is not one anybody can read, so write the entries first"
149
+ )
150
+ lines[start : start + 1] = [UNRELEASED, "", f"## {version}"]
151
+ return "\n".join(lines)
152
+
153
+
154
+ def git(root: Path, *args: str) -> str:
155
+ result = subprocess.run(
156
+ ["git", *args], cwd=root, capture_output=True, text=True, check=True
157
+ )
158
+ return result.stdout
159
+
160
+
161
+ def last_tag(root: Path) -> str | None:
162
+ """The newest `v*` tag reachable from HEAD, or None in a repository with no release.
163
+
164
+ Reachability rather than a sort over every tag: a tag on some other branch is not
165
+ what this history was released as, and would silently truncate the window.
166
+ """
167
+ result = subprocess.run(
168
+ ["git", "describe", "--tags", "--abbrev=0", "--match", "v*"],
169
+ cwd=root,
170
+ capture_output=True,
171
+ text=True,
172
+ )
173
+ return result.stdout.strip() if result.returncode == 0 else None
174
+
175
+
176
+ def subjects_since(root: Path, tag: str | None) -> list[str]:
177
+ span = f"{tag}..HEAD" if tag else "HEAD"
178
+ return [line for line in git(root, "log", "--format=%s", span).splitlines() if line]
179
+
180
+
181
+ def plan(root: Path, init_text: str, requested: str | None) -> tuple[str, str, str | None, list[str]]:
182
+ """The whole decision, before a byte of the tree is touched."""
183
+ current = read_version(init_text)
184
+ parse_version(current)
185
+
186
+ tag = last_tag(root)
187
+ if tag is not None and tag != f"v{current}":
188
+ raise ReleaseError(
189
+ f"the tree declares {current} and the newest tag reachable is {tag}. A bump "
190
+ f"has landed whose tag never did — push v{current} at the commit that moved "
191
+ "it rather than bumping past a version nobody released"
192
+ )
193
+
194
+ subjects = subjects_since(root, tag)
195
+ if not subjects:
196
+ raise ReleaseError(f"nothing has landed since {tag or 'the start of the history'}")
197
+
198
+ if requested:
199
+ version = requested
200
+ if parse_version(version) <= parse_version(current):
201
+ raise ReleaseError(f"{version} is not ahead of {current}")
202
+ else:
203
+ version = next_version(current, subjects)
204
+
205
+ if f"v{version}" in git(root, "tag", "--list").split():
206
+ raise ReleaseError(f"v{version} already exists — PyPI would refuse it as a duplicate")
207
+
208
+ return current, version, tag, subjects
209
+
210
+
211
+ def run(root: Path, requested: str | None) -> None:
212
+ """Decide, rewrite, report. Every refusal leaves this by raising `ReleaseError`."""
213
+ init_text = (root / INIT_PATH).read_text(encoding="utf-8")
214
+ changelog_text = (root / CHANGELOG_PATH).read_text(encoding="utf-8")
215
+
216
+ current, version, tag, subjects = plan(root, init_text, requested)
217
+ # Both rewrites are computed before either is written, so a refusal from the second
218
+ # one does not leave the first one on disk.
219
+ new_init = rewrite_version(init_text, version)
220
+ new_changelog = rewrite_changelog(changelog_text, version)
221
+
222
+ (root / INIT_PATH).write_text(new_init, encoding="utf-8")
223
+ (root / CHANGELOG_PATH).write_text(new_changelog, encoding="utf-8")
224
+
225
+ # Read back rather than trust the substitution: this string is what `release.yml` will
226
+ # look for as `dist/divejson-$version.tar.gz`, and the tag is pushed before anything
227
+ # builds. `read_version` raises here if the line it wrote is no longer one line.
228
+ written = read_version((root / INIT_PATH).read_text(encoding="utf-8"))
229
+ if written != version:
230
+ raise ReleaseError(f"wrote {written!r}, meant {version!r}")
231
+
232
+ print(f"released so far: {current} (tag {tag or 'none'})")
233
+ print(f"releasing: {version} ({bump_kind(subjects)})")
234
+ print(f"window: {len(subjects)} commits")
235
+ for subject in subjects:
236
+ print(f" {subject}")
237
+
238
+ # Only once everything above held: the workflow names the branch, the pull request and
239
+ # the tag from this, so a version here is a commitment to push it.
240
+ output = os.environ.get("GITHUB_OUTPUT")
241
+ if output:
242
+ with open(output, "a", encoding="utf-8") as handle:
243
+ handle.write(f"version={version}\n")
244
+ handle.write(f"previous={current}\n")
245
+
246
+
247
+ def main(argv: Sequence[str] | None = None) -> int:
248
+ parser = argparse.ArgumentParser(description=__doc__)
249
+ parser.add_argument(
250
+ "--version",
251
+ default="",
252
+ help="the version to release. Empty — which is what the workflow passes when its "
253
+ "own input was left blank — computes it from the commit subjects.",
254
+ )
255
+ parser.add_argument(
256
+ "--root",
257
+ type=Path,
258
+ default=Path.cwd(),
259
+ help="the checkout to work in (default: the working directory)",
260
+ )
261
+ args = parser.parse_args(argv)
262
+
263
+ try:
264
+ # Stripped once, here: whitespace around a version typed into the dispatch form is
265
+ # a typo rather than an intention, and every check below is on the value that is
266
+ # actually written.
267
+ run(args.root.resolve(), args.version.strip() or None)
268
+ except ReleaseError as error:
269
+ prefix = "::error::" if os.environ.get("GITHUB_ACTIONS") else "error: "
270
+ print(f"{prefix}{error}", file=sys.stderr)
271
+ return 1
272
+ return 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())
@@ -0,0 +1,119 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ tests:
10
+ name: Tests and the vendored corpus
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ # The floor, a current version, and the one the applications that install this
15
+ # package run. 3.10's stricter datetime.fromisoformat is exactly the kind of
16
+ # thing only running the floor catches, and until 3.14 was in this matrix the
17
+ # converter had never been run on the interpreter its first consumer uses.
18
+ python-version: ["3.10", "3.12", "3.14"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ # Editable, which is the install that has no `divejson/_schema/`: a `force-include`
26
+ # reaches a built wheel and not an editable install, so this leg is what exercises
27
+ # the vendored corpus as the schema's source. The `wheel` job below covers the other.
28
+ - name: Install the package
29
+ run: pip install -e ".[dev]"
30
+
31
+ - name: The tests pass
32
+ run: pytest
33
+
34
+ # The conformance runner over this repository's own copy of the corpus, exactly as
35
+ # the specification repository runs it over the original against a pinned release.
36
+ # `--strict`, so that a format this implementation reads and the corpus has no
37
+ # pairs for fails the build rather than warning into a log nobody reads.
38
+ - name: The vendored corpus conforms
39
+ run: divejson conform fixtures --strict
40
+
41
+ specification:
42
+ name: The vendored tree is the specification at SPEC_REF
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+
47
+ - name: Check the specification out at SPEC_REF
48
+ run: |
49
+ ref="$(tr -d '[:space:]' < SPEC_REF)"
50
+ echo "SPEC_REF=$ref" >> "$GITHUB_ENV"
51
+ git clone --quiet https://github.com/divejson/divejson.git "$RUNNER_TEMP/spec"
52
+ git -C "$RUNNER_TEMP/spec" checkout --quiet "$ref"
53
+
54
+ # A subset check, not an equality one: this repository may carry pairs and mapping
55
+ # documents the specification has not adopted yet, and that is how an adapter lands
56
+ # self-contained. What it may never do is *change* a file the specification owns —
57
+ # that always goes to the specification first, and comes back on the next pin bump.
58
+ - name: Every file the specification owns is byte-identical here
59
+ run: |
60
+ differing=0
61
+ while IFS= read -r file; do
62
+ if ! cmp -s "$RUNNER_TEMP/spec/$file" "$file"; then
63
+ echo "::error file=$file::differs from the specification at $SPEC_REF"
64
+ differing=1
65
+ fi
66
+ done < <(git -C "$RUNNER_TEMP/spec" ls-files -- schema fixtures docs)
67
+ exit $differing
68
+
69
+ # A pin on a commit that never landed — a branch someone force-pushed away, a PR
70
+ # head that was rewritten — would leave this implementing a specification nobody
71
+ # else has, and the byte check above would pass happily.
72
+ - name: SPEC_REF is an ancestor of the specification's main
73
+ run: git -C "$RUNNER_TEMP/spec" merge-base --is-ancestor "$SPEC_REF" origin/main
74
+
75
+ wheel:
76
+ name: An installed wheel carries its own schema
77
+ runs-on: ubuntu-latest
78
+ steps:
79
+ - uses: actions/checkout@v4
80
+ - uses: actions/setup-python@v5
81
+ with:
82
+ python-version: "3.12"
83
+ - run: pip install build hatchling
84
+
85
+ # `python -m build` builds the sdist and then builds the wheel *from the sdist*,
86
+ # which is the artefact question worth asking: a wheel built from the source tree
87
+ # can carry files the sdist forgot. `--no-isolation`, with the backend installed
88
+ # above, so nothing is fetched while building — an sdist that needed the network
89
+ # would make that fetch part of every `pip install` from source.
90
+ - name: Build the sdist, and the wheel from the sdist
91
+ run: python -m build --no-isolation
92
+
93
+ - name: Install the wheel into a virtualenv of its own
94
+ run: |
95
+ python -m venv "$RUNNER_TEMP/venv"
96
+ "$RUNNER_TEMP/venv/bin/pip" install dist/*.whl
97
+
98
+ # Run from `$RUNNER_TEMP`, where there is no checkout to fall back to: this is the
99
+ # position every application that installs the package is in.
100
+ - name: The installed package resolves the schema it carries, with no checkout near
101
+ working-directory: ${{ runner.temp }}
102
+ run: |
103
+ cp "$GITHUB_WORKSPACE/fixtures/valid/technical-dive.divejson" .
104
+ "$RUNNER_TEMP/venv/bin/divejson" validate technical-dive.divejson
105
+ "$RUNNER_TEMP/venv/bin/divejson" conform "$GITHUB_WORKSPACE/fixtures" --strict
106
+ "$RUNNER_TEMP/venv/bin/python" - <<'EOF'
107
+ import os, pathlib
108
+ import divejson
109
+
110
+ installed = pathlib.Path(divejson.__file__).resolve().parent
111
+ assert "site-packages" in str(installed), installed
112
+ vendored = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "schema"
113
+ sources = sorted(vendored.glob("*/divejson.schema.json"))
114
+ assert sources, "the vendored tree carries no schema at all"
115
+ for source in sources:
116
+ packaged = installed / "_schema" / source.parent.name / "divejson.schema.json"
117
+ assert packaged.read_bytes() == source.read_bytes(), packaged
118
+ print(f"schemas checked against the vendored tree: {len(sources)}")
119
+ EOF
@@ -0,0 +1,65 @@
1
+ name: PR Title
2
+
3
+ on:
4
+ pull_request:
5
+ # `edited` is the one that matters: it re-runs the check when a title is
6
+ # corrected, so a failing PR goes green without an empty commit to push.
7
+ #
8
+ # `synchronize` is not about the title at all - it is what would let this be
9
+ # a required check. A required check has to have passed on the PR's *latest*
10
+ # head SHA, so without a run per push, a PR that gains a second commit would
11
+ # sit at "Expected - waiting for status" for good. The title is read from the
12
+ # event payload rather than from the tree, so the extra runs reach the same
13
+ # verdict. Nothing is required on `main` today; this keeps the option open at
14
+ # no cost.
15
+ types: [opened, edited, reopened, synchronize]
16
+
17
+ # The one list of types. CONTRIBUTING.md spells the same list out in prose -
18
+ # keep the two in step.
19
+ env:
20
+ TYPES: feat|fix|refactor|docs|test|chore|perf|ci|build|revert
21
+
22
+ jobs:
23
+ semantic-title:
24
+ runs-on: ubuntu-latest
25
+
26
+ # Nothing is checked out and no API call is made - the title arrives in the
27
+ # event payload - so this job needs no token at all.
28
+ permissions: {}
29
+
30
+ steps:
31
+ # `main`'s ruleset allows only squash merges, and the repository's squash
32
+ # subject source is PR_TITLE rather than COMMIT_OR_PR_TITLE, so this title
33
+ # *is* the commit subject that lands. It is the only part of a branch that
34
+ # outlives the branch, which is why it is checked and the individual
35
+ # commits are not. That setting was corrected by hand after a non-semantic
36
+ # subject reached a default branch in this org; this check is the half that
37
+ # stops the bad title from being written in the first place.
38
+ #
39
+ # The title is passed through `env` rather than interpolated into the
40
+ # script. `${{ github.event.pull_request.title }}` written inside `run:`
41
+ # is substituted before bash parses the line, so a title containing
42
+ # backticks or `$(...)` would execute as shell.
43
+ - name: Check title is a conventional commit subject
44
+ env:
45
+ TITLE: ${{ github.event.pull_request.title }}
46
+ run: |
47
+ PATTERN="^($TYPES)(\([a-z0-9._-]+\))?!?: .+"
48
+
49
+ if printf '%s' "$TITLE" | grep -Eq "$PATTERN"; then
50
+ echo "OK: $TITLE"
51
+ exit 0
52
+ fi
53
+
54
+ echo "PR title is not a semantic commit subject:"
55
+ echo " $TITLE"
56
+ echo
57
+ echo "Expected: <type>[(scope)][!]: <description>"
58
+ echo "Types: ${TYPES//|/, }"
59
+ echo
60
+ echo "Examples: feat(schema): a cylinder may record a mix without a vessel"
61
+ echo " fix(spec): profile.duration spans the samples, not the events"
62
+ echo " docs(spec)!: the draft becomes 1.0"
63
+ echo
64
+ echo "Edit the title - this check re-runs on edit, no push needed."
65
+ exit 1