bitexoft-verify 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,103 @@
1
+ """The `bitexoft.toml` manifest.
2
+
3
+ This is what decouples the checks from the learner's code. Without it, verifying would
4
+ mean imposing package names and paths: "your package is called chaoslab and your domain
5
+ lives in chaoslab/domain". With it, everyone organises their project as they like and
6
+ declares where they put each thing; what is not negotiable is the SHAPE of the
7
+ boundaries, not their names.
8
+
9
+ [project]
10
+ id = "chaos-sports-lab"
11
+ package = "chaoslab"
12
+
13
+ [layers]
14
+ domain = "chaoslab.domain"
15
+
16
+ [cli]
17
+ module = "chaoslab" # run as `python -m chaoslab`
18
+ # main = "main.py" # or a script, if that is your entry point
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ # tomllib joined the standard library in 3.11. Before that the same library is `tomli`,
27
+ # which is declared as a conditional dependency. The learner should not have to upgrade
28
+ # their interpreter to read a six-line configuration file.
29
+ try:
30
+ import tomllib
31
+ except ModuleNotFoundError: # pragma: no cover - exercised on Python 3.10
32
+ import tomli as tomllib # type: ignore[no-redef]
33
+
34
+ MANIFEST_NAME = "bitexoft.toml"
35
+
36
+
37
+ class ManifestError(Exception):
38
+ """The manifest is missing, is not valid TOML, or lacks required keys."""
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class Manifest:
43
+ root: Path
44
+ data: dict
45
+
46
+ @property
47
+ def project_id(self) -> str:
48
+ return str(self.get("project.id", ""))
49
+
50
+ def get(self, dotted: str, default: object | None = None) -> object | None:
51
+ node: object = self.data
52
+ for part in dotted.split("."):
53
+ if not isinstance(node, dict) or part not in node:
54
+ return default
55
+ node = node[part]
56
+ return node
57
+
58
+ def require(self, dotted: str) -> str:
59
+ value = self.get(dotted)
60
+ if not isinstance(value, str) or value.strip() == "":
61
+ raise ManifestError(f"key '{dotted}' is missing from {MANIFEST_NAME}")
62
+ return value
63
+
64
+ def missing(self, keys: list[str]) -> list[str]:
65
+ return [k for k in keys if not isinstance(self.get(k), str) or str(self.get(k)).strip() == ""]
66
+
67
+ def layer(self, name: str) -> str | None:
68
+ value = self.get(f"layers.{name}")
69
+ return value if isinstance(value, str) else None
70
+
71
+ def layer_path(self, name: str) -> Path | None:
72
+ """A layer's folder, resolved from its dotted module path."""
73
+ module = self.layer(name)
74
+ if not module:
75
+ return None
76
+ candidate = self.root / Path(*module.split("."))
77
+ return candidate if candidate.is_dir() else None
78
+
79
+ def resolve(self, reference: str) -> str:
80
+ """Expands contract placeholders: `<layers.domain>.models` -> `chaoslab.domain.models`."""
81
+ out = reference
82
+ while "<" in out and ">" in out:
83
+ start, end = out.index("<"), out.index(">")
84
+ key = out[start + 1 : end]
85
+ value = self.get(key)
86
+ if not isinstance(value, str):
87
+ raise ManifestError(f"the contract uses '{key}' and the manifest does not declare it")
88
+ out = out[:start] + value + out[end + 1 :]
89
+ return out
90
+
91
+
92
+ def load(root: Path) -> Manifest:
93
+ path = root / MANIFEST_NAME
94
+ if not path.is_file():
95
+ raise ManifestError(
96
+ f"{MANIFEST_NAME} not found in {root}. Create it at the repository root: "
97
+ "declare your package and where each layer lives."
98
+ )
99
+ try:
100
+ data = tomllib.loads(path.read_text(encoding="utf-8"))
101
+ except tomllib.TOMLDecodeError as exc:
102
+ raise ManifestError(f"{MANIFEST_NAME} is not valid TOML: {exc}") from exc
103
+ return Manifest(root=root, data=data)
@@ -0,0 +1,234 @@
1
+ """The report: the only thing that leaves this tool and the only thing the platform reads.
2
+
3
+ The same object is written locally and in CI. What changes between the two is
4
+ `provenance.surface`, and all the difference in authority follows from it:
5
+
6
+ local -> informative, for working with answers in seconds
7
+ ci -> official, run pinned and in a clean environment
8
+
9
+ The schema is versioned on purpose: the platform rejects what it does not recognise
10
+ instead of half-interpreting it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime, timezone
20
+ from numbers import Real
21
+ from pathlib import Path
22
+ from typing import Literal
23
+
24
+ from .codes import ISSUE_CODES
25
+
26
+ # Still v3 (ADR 0028): `issues` is optional, a report without it is valid, and a copy of the
27
+ # platform that does not know the field ignores it.
28
+ SCHEMA = "bitexoft.verify.report/v3"
29
+
30
+ Status = Literal["pass", "fail", "skip"]
31
+ Surface = Literal["local", "ci"]
32
+
33
+ Scalar = str | int | float | bool
34
+ Param = Scalar | list
35
+
36
+ CODE_PATTERN = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")
37
+
38
+
39
+ def _plain(value: object, nested: bool = False) -> Param:
40
+ """A value as plain JSON: text, a number, a boolean, or a flat list of those."""
41
+ if isinstance(value, str | bool | int | float):
42
+ return value
43
+ if isinstance(value, list | tuple) and not nested:
44
+ return [_plain(item, nested=True) for item in value]
45
+ if isinstance(value, Real):
46
+ return float(value)
47
+ return str(value)
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class Issue:
52
+ """One problem a check found, as data: a stable code and the values behind it.
53
+
54
+ The sentence a learner reads is not here. The platform writes it, in the learner's language,
55
+ from the code and the parameters (ADR 0027); the CLI writes an English one for the terminal.
56
+ A wrong code or a stray parameter is a bug in a check, so it raises.
57
+ """
58
+
59
+ code: str
60
+ params: dict[str, Param] = field(default_factory=dict)
61
+
62
+ def __post_init__(self) -> None:
63
+ if self.code not in ISSUE_CODES:
64
+ raise ValueError(f"unknown issue code {self.code!r}: add it to bitexoft_verify/codes.py")
65
+ if not CODE_PATTERN.match(self.code):
66
+ raise ValueError(f"issue code {self.code!r} is not <area>.<kind> in lower case")
67
+ stray = set(self.params) - set(ISSUE_CODES[self.code])
68
+ if stray:
69
+ raise ValueError(f"issue {self.code!r} does not take {', '.join(sorted(stray))}")
70
+ # What the learner's code returns is not ours to reject: a value that is not plain JSON
71
+ # (a numpy float, an object) becomes a number or text, so describing a failure never
72
+ # becomes a second failure.
73
+ object.__setattr__(self, "params", {name: _plain(value) for name, value in self.params.items()})
74
+
75
+ def as_dict(self) -> dict:
76
+ return {"code": self.code, "params": dict(self.params)}
77
+
78
+
79
+ def issue(code: str, **params: Param) -> Issue:
80
+ return Issue(code, params)
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class Check:
85
+ """One concrete check. `message` explains a failure or justifies a skip.
86
+
87
+ `title` is always English: it is emitted by the tool, and the platform localises
88
+ known check ids, falling back to this text. `issues` says the same as `message` as data, one
89
+ entry per problem (ADR 0028); `message` stays, in English, for the terminal and as the fallback.
90
+ """
91
+
92
+ id: str
93
+ title: str
94
+ status: Status
95
+ message: str | None = None
96
+ issues: tuple[Issue, ...] = ()
97
+
98
+ def as_dict(self) -> dict:
99
+ data: dict = {"id": self.id, "title": self.title, "status": self.status}
100
+ if self.message:
101
+ data["message"] = self.message
102
+ if self.issues:
103
+ data["issues"] = [item.as_dict() for item in self.issues]
104
+ return data
105
+
106
+
107
+ class Problems:
108
+ """What one check found wrong: the sentence for the terminal and the issues that say it as data.
109
+
110
+ Each entry keeps its sentence and its issues together, where the problem is found, so the
111
+ message and the issues cannot drift apart. `failing` builds the check with the same sentence the
112
+ check always produced.
113
+ """
114
+
115
+ def __init__(self) -> None:
116
+ self._entries: list[tuple[str, tuple[Issue, ...]]] = []
117
+
118
+ def add(self, text: str, code: str, **params: Param) -> None:
119
+ self._entries.append((text, (Issue(code, params),)))
120
+
121
+ def add_group(self, text: str, issues: list[Issue]) -> None:
122
+ """Several issues told in one sentence."""
123
+ self._entries.append((text, tuple(issues)))
124
+
125
+ def __bool__(self) -> bool:
126
+ return bool(self._entries)
127
+
128
+ def failing(self, check_id: str, title: str, separator: str = " | ", limit: int | None = None) -> Check:
129
+ entries = self._entries[:limit] if limit is not None else self._entries
130
+ return Check(
131
+ check_id,
132
+ title,
133
+ "fail",
134
+ separator.join(text for text, _ in entries),
135
+ tuple(item for _, issues in entries for item in issues),
136
+ )
137
+
138
+
139
+ @dataclass(slots=True)
140
+ class Gate:
141
+ """A group of checks. The gate fails if any of them fails."""
142
+
143
+ id: str
144
+ checks: list[Check] = field(default_factory=list)
145
+
146
+ @property
147
+ def status(self) -> Status:
148
+ if any(check.status == "fail" for check in self.checks):
149
+ return "fail"
150
+ if self.checks and all(check.status == "skip" for check in self.checks):
151
+ return "skip"
152
+ return "pass"
153
+
154
+ def as_dict(self) -> dict:
155
+ return {"id": self.id, "status": self.status, "checks": [check.as_dict() for check in self.checks]}
156
+
157
+
158
+ def detect_surface(explicit: str | None = None) -> Surface:
159
+ """CI detects itself: nobody has to remember a flag."""
160
+ if explicit in ("local", "ci"):
161
+ return explicit # type: ignore[return-value]
162
+ return "ci" if os.environ.get("GITHUB_ACTIONS") == "true" else "local"
163
+
164
+
165
+ def detect_provenance(surface: Surface) -> dict:
166
+ env = os.environ
167
+ repo = env.get("GITHUB_REPOSITORY")
168
+ run_id = env.get("GITHUB_RUN_ID")
169
+ server = env.get("GITHUB_SERVER_URL", "https://github.com")
170
+ return {
171
+ "surface": surface,
172
+ "repo": repo,
173
+ "commit": env.get("GITHUB_SHA"),
174
+ "run_url": f"{server}/{repo}/actions/runs/{run_id}" if repo and run_id else None,
175
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
176
+ }
177
+
178
+
179
+ @dataclass(slots=True)
180
+ class Report:
181
+ project: str
182
+ delivery: str
183
+ cli_version: str
184
+ gates: list[Gate]
185
+ surface: Surface = "local"
186
+ # Reserved for the graduated-help mechanism. `help_level` is the highest level of
187
+ # assistance taken for this delivery (0 = none) and `provided_files` lists files the
188
+ # learner did not write. They are declared now, and always emitted, so the platform
189
+ # never has to deal with two report shapes once help ships.
190
+ help_level: int = 0
191
+ provided_files: list[str] = field(default_factory=list)
192
+
193
+ @property
194
+ def failed(self) -> int:
195
+ return sum(1 for gate in self.gates for check in gate.checks if check.status == "fail")
196
+
197
+ @property
198
+ def passed(self) -> int:
199
+ return sum(1 for gate in self.gates for check in gate.checks if check.status == "pass")
200
+
201
+ @property
202
+ def skipped(self) -> int:
203
+ return sum(1 for gate in self.gates for check in gate.checks if check.status == "skip")
204
+
205
+ @property
206
+ def total(self) -> int:
207
+ return sum(len(gate.checks) for gate in self.gates)
208
+
209
+ @property
210
+ def ok(self) -> bool:
211
+ return self.failed == 0
212
+
213
+ def as_dict(self) -> dict:
214
+ return {
215
+ "schema": SCHEMA,
216
+ "cli_version": self.cli_version,
217
+ "project": self.project,
218
+ "delivery": self.delivery,
219
+ "provenance": detect_provenance(self.surface),
220
+ "autonomy": {"help_level": self.help_level, "provided_files": sorted(self.provided_files)},
221
+ "gates": [gate.as_dict() for gate in self.gates],
222
+ "summary": {
223
+ "status": "pass" if self.ok else "fail",
224
+ "passed": self.passed,
225
+ "failed": self.failed,
226
+ "skipped": self.skipped,
227
+ "total": self.total,
228
+ },
229
+ }
230
+
231
+ def write(self, path: Path) -> Path:
232
+ path.parent.mkdir(parents=True, exist_ok=True)
233
+ path.write_text(json.dumps(self.as_dict(), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
234
+ return path
@@ -0,0 +1,101 @@
1
+ """Picks and runs the checks for a delivery.
2
+
3
+ Verification is CUMULATIVE: verifying A3 also runs A1 and A2. That is what makes the
4
+ method agile rather than merely incremental — every delivery leaves the whole project
5
+ standing, and a regression in old work surfaces while verifying the new.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ from dataclasses import replace
13
+ from pathlib import Path
14
+
15
+ from .checks import a1
16
+ from .checks.common import Context
17
+ from .manifest import Manifest, load
18
+ from .report import Check, Gate, Report
19
+
20
+ # Deliveries in the order they are meant to be built. Adding one is adding a line here
21
+ # plus its module; nothing else in the tool needs to know.
22
+ DELIVERIES: dict[str, object] = {"A1": a1}
23
+ DELIVERY_ID = re.compile(r"^[A-G][0-9]{1,2}$")
24
+
25
+ # Deliveries whose checks are frozen (ADR 0009). Some of A1's checks are the shared ones in
26
+ # `checks/common.py`, which can describe a failure as structured issues (ADR 0028). A frozen
27
+ # delivery's results must not change, so they are emitted without `issues`: the `message` and
28
+ # the statuses are exactly what they always were, and the platform shows that message as it is.
29
+ # Taking A1 out of this set is a decision for a new ADR, not a tidy-up.
30
+ FROZEN_DELIVERIES = frozenset({"A1"})
31
+
32
+
33
+ class UnknownDelivery(Exception):
34
+ """That delivery has no published checks yet."""
35
+
36
+
37
+ def available_deliveries() -> list[str]:
38
+ return list(DELIVERIES)
39
+
40
+
41
+ def _prepare_imports(root: Path) -> None:
42
+ """The learner's code wins: their root goes first on sys.path."""
43
+ entry = str(root.resolve())
44
+ if entry in sys.path:
45
+ sys.path.remove(entry)
46
+ sys.path.insert(0, entry)
47
+
48
+
49
+ def _retag(check: Check, delivery_id: str) -> Check:
50
+ """Marks an inherited check so a regression says which delivery it came from."""
51
+ return Check(
52
+ id=f"{delivery_id.lower()}_{check.id}",
53
+ title=f"[{delivery_id}] {check.title}",
54
+ status=check.status,
55
+ message=check.message,
56
+ issues=check.issues,
57
+ )
58
+
59
+
60
+ def run(
61
+ root: Path,
62
+ delivery_id: str,
63
+ *,
64
+ cli_version: str,
65
+ surface: str = "local",
66
+ cumulative: bool = True,
67
+ manifest: Manifest | None = None,
68
+ timeout: int = 180,
69
+ ) -> Report:
70
+ if not DELIVERY_ID.match(delivery_id):
71
+ raise UnknownDelivery(f"'{delivery_id}' is not a delivery identifier, such as A1")
72
+ if delivery_id not in DELIVERIES:
73
+ raise UnknownDelivery(
74
+ f"delivery {delivery_id} has no published checks yet "
75
+ f"(available: {', '.join(available_deliveries())}). Update bitexoft-verify."
76
+ )
77
+
78
+ manifest = manifest or load(root)
79
+ _prepare_imports(root)
80
+ ctx = Context(root=root, manifest=manifest, timeout=timeout)
81
+
82
+ order = available_deliveries()
83
+ wanted = order[: order.index(delivery_id) + 1] if cumulative else [delivery_id]
84
+
85
+ merged: dict[str, Gate] = {}
86
+ for current in wanted:
87
+ module = DELIVERIES[current]
88
+ for gate in module.gates(ctx): # type: ignore[attr-defined]
89
+ target = merged.setdefault(gate.id, Gate(gate.id))
90
+ for check in gate.checks:
91
+ if current in FROZEN_DELIVERIES and check.issues:
92
+ check = replace(check, issues=())
93
+ target.checks.append(check if current == delivery_id else _retag(check, current))
94
+
95
+ return Report(
96
+ project=manifest.project_id or "unknown",
97
+ delivery=delivery_id,
98
+ cli_version=cli_version,
99
+ gates=[merged[key] for key in ("acceptance", "design", "evidence") if key in merged],
100
+ surface=surface, # type: ignore[arg-type]
101
+ )
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: bitexoft-verify
3
+ Version: 0.2.0
4
+ Summary: Verifica las entregas de los proyectos de Bitexoft: la misma herramienta en tu máquina y en CI.
5
+ Author: Bitexoft
6
+ License: Proprietary
7
+ Keywords: ai,api,education,python,verification
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Education :: Testing
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: tomli>=2.0.1; python_version < '3.11'
16
+ Provides-Extra: api
17
+ Requires-Dist: httpx>=0.27; extra == 'api'
18
+ Requires-Dist: pyyaml>=6.0; extra == 'api'
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.6; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # bitexoft-verify
25
+
26
+ The platform's only definition of "correct". It runs in two places and decides the same
27
+ thing in both:
28
+
29
+ | Where | When | What it is worth |
30
+ |---|---|---|
31
+ | Your machine | while you work, answers in seconds | informative |
32
+ | GitHub Actions | on every push, pinned, clean environment | **official** |
33
+
34
+ The difference shows up as `provenance.surface` in the report. The platform marks a
35
+ delivery as verified only on a green CI report.
36
+
37
+ ## Install
38
+
39
+ Python 3.10 or newer — the version Ubuntu 22.04 LTS ships, so a stock Linux works without
40
+ upgrading anything.
41
+
42
+ ```bash
43
+ pip install bitexoft-verify # core; one small dependency below 3.11
44
+ pip install 'bitexoft-verify[api]' # adds what later deliveries need to verify APIs
45
+ ```
46
+
47
+ If pip answers `requires a different Python`, check your version with `python3 -V`. The
48
+ project itself targets a newer Python than the tool does: each delivery's contract states
49
+ what it needs.
50
+
51
+ ## Use
52
+
53
+ ```bash
54
+ bitexoft-verify check A1 # verify A1
55
+ bitexoft-verify check A3 # verify A3 and, on the way, A1 and A2
56
+ bitexoft-verify check A3 --only # just A3
57
+ bitexoft-verify deliveries # what this version can verify
58
+ ```
59
+
60
+ Exit codes: `0` all green, `1` something fails, `2` could not even start.
61
+
62
+ ## `bitexoft.toml`
63
+
64
+ Organise your project however you like. You declare it here and verification reads it
65
+ from here; what is not negotiable is the **shape** of the boundaries, not their names.
66
+
67
+ ```toml
68
+ [project]
69
+ id = "chaos-sports-lab"
70
+ package = "chaoslab"
71
+
72
+ [layers]
73
+ domain = "chaoslab.domain"
74
+
75
+ [cli]
76
+ module = "chaoslab" # run as `python -m chaoslab`
77
+ # main = "main.py" # or a script, if that is your entry point
78
+ ```
79
+
80
+ ## The three gates
81
+
82
+ - **Acceptance** — that it works: the contract's symbols, impossible objects refused,
83
+ value semantics, the geometry, the entry point and its exit codes.
84
+ - **Design** — that it is built well: the domain's boundary read from the syntax tree,
85
+ your tests passing, at least one test that builds an impossible object.
86
+ - **Evidence** — that you understand it. The CLI **does not judge** this gate: it always
87
+ reports `skip` with a message. You close it on the platform by writing the why and
88
+ comparing it against the rubric.
89
+
90
+ A check never fails for something you cannot fix: if an optional dependency or a git tag
91
+ is missing, it **skips** with a message saying what to do.
92
+
93
+ ## Cumulative verification
94
+
95
+ Verifying A3 also runs A1 and A2, with inherited checks tagged `[A1]`. That is what makes
96
+ the method agile: every delivery leaves the whole project standing, and a regression in
97
+ old work shows up while verifying the new.
98
+
99
+ ## The sample files
100
+
101
+ Deliveries are verified against files the CLI ships with, not against one you wrote. For
102
+ A1 that is 30 instants crossing all three block states, plus a copy with one coordinate
103
+ off the pitch to check that bad data fails cleanly instead of printing a traceback at the
104
+ user. They are synthetic: no third-party data is distributed here.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ pip install -e '.[dev,api]'
110
+ pytest # builds a fake learner repository and verifies it
111
+ ruff check .
112
+ python tools/make_fixtures.py # regenerate the sample files
113
+ ```
114
+
115
+ Before changing what a check says when it fails, or the layout of the output, read
116
+ [`docs/foundations/03-feedback-self-explanation-calibration.md`](../docs/foundations/03-feedback-self-explanation-calibration.md)
117
+ (the verifier's messages are the feedback a learner reads) and ADR 0002. The check ids and the report
118
+ schema are a contract with the platform: do not rename them.
119
+
120
+ ## Releasing
121
+
122
+ The version is written once, in `__version__` (`src/bitexoft_verify/__init__.py`). The pin in
123
+ `.github/workflows/verify.yml` (`cli-version`) must match its `major.minor`; a test checks it.
124
+
125
+ First release, by hand on pypi.org:
126
+
127
+ 1. Account → Publishing → *Add a pending publisher*: project `bitexoft-verify`, owner `bitexoft`,
128
+ repository `sports-lab-v1`, workflow `release-verify.yml`, environment `pypi`.
129
+ 2. In the repository settings, create the environment `pypi`.
130
+
131
+ Each release:
132
+
133
+ ```bash
134
+ git tag verify-v0.2.0 && git push origin verify-v0.2.0
135
+ ```
136
+
137
+ The workflow runs `pytest` on 3.10 and 3.12, refuses a tag that differs from `__version__`, builds
138
+ with `python -m build` and publishes with trusted publishing (no tokens). Check it in a clean
139
+ environment: `pipx run --spec bitexoft-verify bitexoft-verify deliveries`.
@@ -0,0 +1,19 @@
1
+ bitexoft_verify/__init__.py,sha256=ztmdVEmKeQyrOoF-IvxGkoN9Wr8QpwVhaEXHfiJFKvU,91
2
+ bitexoft_verify/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
+ bitexoft_verify/cli.py,sha256=U5UkgX5gdnRv_J6otLKFNWESzuG7GRG3EqRIJCnbDkA,4609
4
+ bitexoft_verify/codes.py,sha256=mZkelfk8SAxKVxgFEG56Xbhpnn7Sx9ZJF8zGLb9qOVI,2653
5
+ bitexoft_verify/manifest.py,sha256=Rcdi-FBh6-2wxYCkOCyr5SdUW9QFm9Mgt0mnALCtJXM,3728
6
+ bitexoft_verify/report.py,sha256=XkEu0vOwFDZ4tmi4X5kYXmH9v5o9VojulPixpQa-6CI,8538
7
+ bitexoft_verify/runner.py,sha256=9FPLOBeD3phIsxT5mCA1pDWrozADB8UdAoBiaBEa0CM,3612
8
+ bitexoft_verify/checks/__init__.py,sha256=zluliHovi0N9aMMRr6sXhhYonMcLT6UX3Tr6q6v8N-0,39
9
+ bitexoft_verify/checks/a1.py,sha256=ooeSykNKXQCZ-8gcvh4z9yzbPZZdkOTh68UAXsdVwZQ,15716
10
+ bitexoft_verify/checks/common.py,sha256=mDo-LKMldGjmYMvFR_GWcNwO5FgothVh2YjG65sc_aM,10779
11
+ bitexoft_verify/data/README.md,sha256=FBoTovLSIQ6IKhPOtz5iUFbs-QGFed1BwFbMh4inuX8,706
12
+ bitexoft_verify/data/a1_impossible_sample.csv,sha256=kvnTYoEnt82ZFwMUmGd1UOr6qMMsJ8sRHHH40CpDvk4,5298
13
+ bitexoft_verify/data/a1_match_sample.csv,sha256=8NEKo2gvY_Z12-Q1KBdKnSpRV2SPXOOE58VfAccVH74,5298
14
+ bitexoft_verify/data/a2_isolated_sample.csv,sha256=O5iGlwlS1tlmym1axT7PRtuu-QEWvJzWd2BtbEFvps0,5310
15
+ bitexoft_verify/data/issue-codes.json,sha256=Cv77fIn9nfKvzvPh7tiG9NiRyRYDUpLwHq_UCjpnNyI,1002
16
+ bitexoft_verify-0.2.0.dist-info/METADATA,sha256=vPXFttEZ03Oh4PC0p_D-l-wJSzThQqj1ChNiq7oMlm8,5336
17
+ bitexoft_verify-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
+ bitexoft_verify-0.2.0.dist-info/entry_points.txt,sha256=MI-OtsbZc0x0PEh3SbFEr87zUEs01vwuENgDX5Tkvas,61
19
+ bitexoft_verify-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bitexoft-verify = bitexoft_verify.cli:main