codepraxis 0.2.1__tar.gz → 0.3.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.
- {codepraxis-0.2.1 → codepraxis-0.3.0}/PKG-INFO +41 -2
- {codepraxis-0.2.1 → codepraxis-0.3.0}/README.md +40 -1
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/__init__.py +1 -1
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/cli.py +70 -0
- codepraxis-0.3.0/src/codepraxis/commands/example.py +73 -0
- codepraxis-0.3.0/src/codepraxis/commands/lint.py +43 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/commands/validate.py +21 -1
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/remote/client.py +4 -1
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/remote/config.py +11 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/reporting/human.py +10 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/__init__.py +0 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/generator.py +139 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/README.md +7 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/backend.conf +4 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/course_toc.json +8 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/feature.md +21 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/main.py +12 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/metadata.json +3 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/solution.py +12 -0
- codepraxis-0.3.0/src/codepraxis/scaffold/templates/test_1.py +74 -0
- codepraxis-0.3.0/src/codepraxis/validation/__init__.py +0 -0
- codepraxis-0.3.0/src/codepraxis/validation/registry.py +63 -0
- codepraxis-0.3.0/src/codepraxis/validation/rule.py +36 -0
- codepraxis-0.3.0/src/codepraxis/validation/rules/__init__.py +0 -0
- codepraxis-0.3.0/src/codepraxis/validation/rules/hygiene.py +98 -0
- codepraxis-0.3.0/src/codepraxis/validation/rules/instructions.py +66 -0
- codepraxis-0.3.0/src/codepraxis/validation/rules/testcases.py +171 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/tests/test_classification.py +31 -0
- codepraxis-0.3.0/tests/test_validation_rules.py +221 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/.gitignore +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/CONTRIBUTING.md +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/RELEASING.md +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/pyproject.toml +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/__main__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/commands/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/commands/login.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/commands/publish.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/domain/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/domain/contract.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/domain/pack.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/domain/results.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/errors.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/executor.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/local/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/local/backends.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/local/executor.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/local/worker.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/local/workspace.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/remote/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/execution/remote/executor.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/packio/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/packio/archive.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/packio/discovery.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/packio/loader.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/packio/toc.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/installer.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/templates/commands/new.md +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/templates/commands/validate.md +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/templates/marketplace.json +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/templates/plugin.json +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/reporting/__init__.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/reporting/json_reporter.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/src/codepraxis/reporting/reporter.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/tests/conformance/test_corpus.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/tests/conftest.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/tests/test_publish.py +0 -0
- {codepraxis-0.2.1 → codepraxis-0.3.0}/tests/test_toc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codepraxis
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Author and validate CodePraxis challenge packs.
|
|
5
5
|
Project-URL: Documentation, https://docs.codepraxis.com/authoring
|
|
6
6
|
Author: CodePraxis
|
|
@@ -29,8 +29,11 @@ Author and validate CodePraxis challenge packs from your own repository.
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
pip install codepraxis
|
|
32
|
+
codepraxis --example # see a real challenge, no account needed
|
|
33
|
+
codepraxis new my-challenge # scaffold one that already validates
|
|
34
|
+
codepraxis lint my-challenge # static checks, no execution
|
|
35
|
+
codepraxis validate --local my-challenge # run it, fast and advisory
|
|
32
36
|
codepraxis --login
|
|
33
|
-
codepraxis validate --local my-challenge # fast, advisory
|
|
34
37
|
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
35
38
|
```
|
|
36
39
|
|
|
@@ -68,6 +71,42 @@ codepraxis validate --local my-challenge
|
|
|
68
71
|
Once a key is configured the leniency stops: a model failure is then a real
|
|
69
72
|
failure, because it can be judged.
|
|
70
73
|
|
|
74
|
+
## Try it first
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codepraxis --example
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Starts a throwaway container with the featured challenge already cloned into it
|
|
81
|
+
and prints a URL. That is exactly what a candidate sees. No login, no account,
|
|
82
|
+
nothing recorded as an attempt. Add `--open` to launch it in your browser.
|
|
83
|
+
|
|
84
|
+
## Starting a new pack
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
codepraxis new my-challenge
|
|
88
|
+
codepraxis validate --local my-challenge # → PASSED
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The generated pack is complete and already passing — solution passes, starter
|
|
92
|
+
fails. Edit `._course_data/feature.md`, `source/`, `._tests/test_1.py` and
|
|
93
|
+
`../solution/` to make it yours.
|
|
94
|
+
|
|
95
|
+
## Two kinds of checking
|
|
96
|
+
|
|
97
|
+
| Command | Runs | Cost |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `codepraxis lint` | Static rules over the files | milliseconds, no execution |
|
|
100
|
+
| `codepraxis validate` | The tests, against both fixtures | seconds (local) or ~1 min (remote) |
|
|
101
|
+
|
|
102
|
+
`lint` never imports pack code, so it is safe on a pack you did not write and
|
|
103
|
+
fast enough for every save. It catches things that otherwise only surface inside
|
|
104
|
+
a container — a two-argument `testCases.__init__`, zero-padded case names, a
|
|
105
|
+
`solution/` directory nested inside the pack, LaTeX in the Instructions brief.
|
|
106
|
+
`validate` runs it automatically first and stops if it finds an error, because
|
|
107
|
+
executing a pack whose class cannot be constructed only produces a confusing
|
|
108
|
+
traceback.
|
|
109
|
+
|
|
71
110
|
## The two fixtures
|
|
72
111
|
|
|
73
112
|
Every pack is validated twice:
|
|
@@ -4,8 +4,11 @@ Author and validate CodePraxis challenge packs from your own repository.
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pip install codepraxis
|
|
7
|
+
codepraxis --example # see a real challenge, no account needed
|
|
8
|
+
codepraxis new my-challenge # scaffold one that already validates
|
|
9
|
+
codepraxis lint my-challenge # static checks, no execution
|
|
10
|
+
codepraxis validate --local my-challenge # run it, fast and advisory
|
|
7
11
|
codepraxis --login
|
|
8
|
-
codepraxis validate --local my-challenge # fast, advisory
|
|
9
12
|
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
10
13
|
```
|
|
11
14
|
|
|
@@ -43,6 +46,42 @@ codepraxis validate --local my-challenge
|
|
|
43
46
|
Once a key is configured the leniency stops: a model failure is then a real
|
|
44
47
|
failure, because it can be judged.
|
|
45
48
|
|
|
49
|
+
## Try it first
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
codepraxis --example
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Starts a throwaway container with the featured challenge already cloned into it
|
|
56
|
+
and prints a URL. That is exactly what a candidate sees. No login, no account,
|
|
57
|
+
nothing recorded as an attempt. Add `--open` to launch it in your browser.
|
|
58
|
+
|
|
59
|
+
## Starting a new pack
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
codepraxis new my-challenge
|
|
63
|
+
codepraxis validate --local my-challenge # → PASSED
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The generated pack is complete and already passing — solution passes, starter
|
|
67
|
+
fails. Edit `._course_data/feature.md`, `source/`, `._tests/test_1.py` and
|
|
68
|
+
`../solution/` to make it yours.
|
|
69
|
+
|
|
70
|
+
## Two kinds of checking
|
|
71
|
+
|
|
72
|
+
| Command | Runs | Cost |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `codepraxis lint` | Static rules over the files | milliseconds, no execution |
|
|
75
|
+
| `codepraxis validate` | The tests, against both fixtures | seconds (local) or ~1 min (remote) |
|
|
76
|
+
|
|
77
|
+
`lint` never imports pack code, so it is safe on a pack you did not write and
|
|
78
|
+
fast enough for every save. It catches things that otherwise only surface inside
|
|
79
|
+
a container — a two-argument `testCases.__init__`, zero-padded case names, a
|
|
80
|
+
`solution/` directory nested inside the pack, LaTeX in the Instructions brief.
|
|
81
|
+
`validate` runs it automatically first and stops if it finds an error, because
|
|
82
|
+
executing a pack whose class cannot be constructed only produces a confusing
|
|
83
|
+
traceback.
|
|
84
|
+
|
|
46
85
|
## The two fixtures
|
|
47
86
|
|
|
48
87
|
Every pack is validated twice:
|
|
@@ -12,6 +12,8 @@ import sys
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from . import __version__
|
|
15
|
+
from .commands import example as example_command
|
|
16
|
+
from .commands import lint as lint_command
|
|
15
17
|
from .commands import login as login_command
|
|
16
18
|
from .commands import publish as publish_command
|
|
17
19
|
from .commands import validate as validate_command
|
|
@@ -21,6 +23,7 @@ from .execution.local.executor import LocalExecutor
|
|
|
21
23
|
from .plugin import installer
|
|
22
24
|
from .reporting.human import HumanReporter
|
|
23
25
|
from .reporting.json_reporter import JsonReporter
|
|
26
|
+
from .scaffold import generator
|
|
24
27
|
|
|
25
28
|
EXIT_USAGE = 2
|
|
26
29
|
|
|
@@ -62,6 +65,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
62
65
|
metavar="TARGET",
|
|
63
66
|
help="Install an integration. Currently: claude-plugin.",
|
|
64
67
|
)
|
|
68
|
+
actions.add_argument(
|
|
69
|
+
"--example",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Open the featured challenge in a live container. No account needed.",
|
|
72
|
+
)
|
|
65
73
|
|
|
66
74
|
parser.add_argument(
|
|
67
75
|
"--root",
|
|
@@ -87,6 +95,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
87
95
|
help="Show passing cases and anything the tier cannot verify.",
|
|
88
96
|
)
|
|
89
97
|
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--open",
|
|
100
|
+
action="store_true",
|
|
101
|
+
dest="open_browser",
|
|
102
|
+
help="With --example, open the container in your browser.",
|
|
103
|
+
)
|
|
90
104
|
|
|
91
105
|
subparsers = parser.add_subparsers(dest="command")
|
|
92
106
|
|
|
@@ -146,6 +160,37 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
146
160
|
check.add_argument("-v", "--verbose", action="store_true", help="Show passing cases and notes.")
|
|
147
161
|
check.set_defaults(handler=_handle_validate)
|
|
148
162
|
|
|
163
|
+
static = subparsers.add_parser(
|
|
164
|
+
"lint",
|
|
165
|
+
help="Static checks only — no execution, no container.",
|
|
166
|
+
description=(
|
|
167
|
+
"Reads the pack and reports problems without importing or running any "
|
|
168
|
+
"of its code. Fast enough to run on every save, and safe on a pack you "
|
|
169
|
+
"did not write."
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
static.add_argument("selector", nargs="?", help="Pack directory or name.")
|
|
173
|
+
static.add_argument("--root", type=Path, default=None, help="Directory to search for packs.")
|
|
174
|
+
static.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
|
175
|
+
static.add_argument("-v", "--verbose", action="store_true", help="Show notes as well as problems.")
|
|
176
|
+
static.set_defaults(handler=_handle_lint)
|
|
177
|
+
|
|
178
|
+
scaffold = subparsers.add_parser(
|
|
179
|
+
"new",
|
|
180
|
+
help="Scaffold a new pack that already validates.",
|
|
181
|
+
description=(
|
|
182
|
+
"Writes a complete, passing pack you can edit: solution passes, starter "
|
|
183
|
+
"fails. Running `codepraxis validate --local` on it should go green "
|
|
184
|
+
"immediately, which also proves your setup works."
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
scaffold.add_argument("name", help="Pack name (lowercase letters, digits, underscores).")
|
|
188
|
+
scaffold.add_argument("--root", type=Path, default=None, help="Where to create it (default: ./challenges).")
|
|
189
|
+
scaffold.add_argument("--backend", default="AI", help="BACKEND value: AI, DSA, EMB or LNX.")
|
|
190
|
+
scaffold.add_argument("--language", default="PYTHON", help="LANGUAGE value.")
|
|
191
|
+
scaffold.add_argument("--force", action="store_true", help="Overwrite an existing directory.")
|
|
192
|
+
scaffold.set_defaults(handler=_handle_new)
|
|
193
|
+
|
|
149
194
|
return parser
|
|
150
195
|
|
|
151
196
|
|
|
@@ -177,6 +222,29 @@ def _handle_validate(args: argparse.Namespace) -> int:
|
|
|
177
222
|
)
|
|
178
223
|
|
|
179
224
|
|
|
225
|
+
def _handle_lint(args: argparse.Namespace) -> int:
|
|
226
|
+
return lint_command.run(
|
|
227
|
+
root=args.root or Path.cwd(),
|
|
228
|
+
selector=args.selector,
|
|
229
|
+
reporter=_reporter(args),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _handle_new(args: argparse.Namespace) -> int:
|
|
234
|
+
# Default to ./challenges so the solution/ sibling lands outside the pack
|
|
235
|
+
# without the author having to think about layout.
|
|
236
|
+
root = args.root or (Path.cwd() / "challenges")
|
|
237
|
+
result = generator.create(
|
|
238
|
+
root=root,
|
|
239
|
+
raw_name=args.name,
|
|
240
|
+
backend=args.backend,
|
|
241
|
+
language=args.language,
|
|
242
|
+
force=args.force,
|
|
243
|
+
)
|
|
244
|
+
print(generator.describe(result, Path.cwd()))
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
|
|
180
248
|
def _handle_install(target: str, force: bool) -> int:
|
|
181
249
|
if target == "claude-plugin":
|
|
182
250
|
result = installer.install(Path.cwd(), force=force)
|
|
@@ -203,6 +271,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
203
271
|
)
|
|
204
272
|
if args.install:
|
|
205
273
|
return _handle_install(args.install, args.force)
|
|
274
|
+
if args.example:
|
|
275
|
+
return example_command.run(open_browser=args.open_browser)
|
|
206
276
|
if not getattr(args, "handler", None):
|
|
207
277
|
parser.print_help()
|
|
208
278
|
return EXIT_USAGE
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""``codepraxis --example`` — open the featured challenge in a live container.
|
|
2
|
+
|
|
3
|
+
The fastest way to understand what a pack becomes: the platform allocates a
|
|
4
|
+
throwaway container, clones the featured challenge into it, and returns a URL
|
|
5
|
+
you can open in a browser to see exactly what a candidate sees.
|
|
6
|
+
|
|
7
|
+
Deliberately unauthenticated. This is the first thing someone runs after
|
|
8
|
+
installing, usually before they have a key, so requiring login would put a
|
|
9
|
+
signup in front of the demo.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import webbrowser
|
|
15
|
+
|
|
16
|
+
from ..errors import PraxisError
|
|
17
|
+
from ..execution.remote.client import ApiClient
|
|
18
|
+
from ..execution.remote.config import RemoteConfig
|
|
19
|
+
|
|
20
|
+
EXIT_OK = 0
|
|
21
|
+
|
|
22
|
+
FEATURED_PATH = "/challenges/featured"
|
|
23
|
+
TRIAL_PATH = "/challenges/trial/setup-codebase"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(client: ApiClient | None = None, open_browser: bool = False) -> int:
|
|
27
|
+
client = client or ApiClient(RemoteConfig.resolve_public())
|
|
28
|
+
|
|
29
|
+
challenge = (client.get(FEATURED_PATH) or {}).get("challenge") or {}
|
|
30
|
+
_describe(challenge)
|
|
31
|
+
|
|
32
|
+
print("\nStarting a container… this usually takes under a minute.")
|
|
33
|
+
session = client.post_json(TRIAL_PATH, {})
|
|
34
|
+
|
|
35
|
+
url = session.get("container_url")
|
|
36
|
+
if not url:
|
|
37
|
+
raise PraxisError("The platform started a session but returned no container URL.")
|
|
38
|
+
|
|
39
|
+
print(f"\n {url}\n")
|
|
40
|
+
_footer(session)
|
|
41
|
+
|
|
42
|
+
if open_browser:
|
|
43
|
+
webbrowser.open(url)
|
|
44
|
+
|
|
45
|
+
return EXIT_OK
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _describe(challenge: dict) -> None:
|
|
49
|
+
name = challenge.get("challenge_name") or "Featured challenge"
|
|
50
|
+
print(name)
|
|
51
|
+
|
|
52
|
+
description = challenge.get("description")
|
|
53
|
+
if description:
|
|
54
|
+
print(f" {str(description).strip().splitlines()[0]}")
|
|
55
|
+
|
|
56
|
+
difficulty = {1: "Easy", 2: "Medium", 3: "Hard"}.get(challenge.get("difficulty"))
|
|
57
|
+
# Drop blanks: the field frequently carries an empty trailing entry.
|
|
58
|
+
stack = [item for item in (challenge.get("tech_stack") or []) if str(item).strip()]
|
|
59
|
+
bits = [bit for bit in (difficulty, ", ".join(stack) if stack else None) if bit]
|
|
60
|
+
if bits:
|
|
61
|
+
print(f" {' · '.join(bits)}")
|
|
62
|
+
|
|
63
|
+
if challenge.get("max_time"):
|
|
64
|
+
print(f" {challenge['max_time']} minutes")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _footer(session: dict) -> None:
|
|
68
|
+
expires = session.get("attempt_expires_at")
|
|
69
|
+
if expires:
|
|
70
|
+
print(f"The container is temporary and expires at {expires}.")
|
|
71
|
+
else:
|
|
72
|
+
print("The container is temporary and will be reclaimed when idle.")
|
|
73
|
+
print("Nothing you do in it is recorded as an attempt.")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""``codepraxis lint`` — static checks only, no execution.
|
|
2
|
+
|
|
3
|
+
Fast enough to run on every save, and safe to run against a pack you did not
|
|
4
|
+
write, because no pack code is imported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..domain.results import RunResult
|
|
12
|
+
from ..errors import PackError
|
|
13
|
+
from ..packio.discovery import find_packs, resolve_pack_dir
|
|
14
|
+
from ..packio.loader import load_pack
|
|
15
|
+
from ..reporting.reporter import Reporter
|
|
16
|
+
from ..validation.registry import has_errors
|
|
17
|
+
from ..validation.registry import lint as run_rules
|
|
18
|
+
|
|
19
|
+
EXIT_OK = 0
|
|
20
|
+
EXIT_FAILED = 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(root: Path, selector: str | None, reporter: Reporter) -> int:
|
|
24
|
+
pack_dirs = [resolve_pack_dir(root, selector)] if selector else find_packs(root)
|
|
25
|
+
if not pack_dirs:
|
|
26
|
+
raise PackError(
|
|
27
|
+
f"No challenge packs found under {root}. "
|
|
28
|
+
f"A pack is a directory containing metadata.json and backend.conf."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
exit_code = EXIT_OK
|
|
32
|
+
try:
|
|
33
|
+
for pack_dir in pack_dirs:
|
|
34
|
+
pack = load_pack(pack_dir)
|
|
35
|
+
findings = run_rules(pack)
|
|
36
|
+
reporter.report(
|
|
37
|
+
RunResult(pack_name=pack.name, executor="lint", diagnostics=tuple(findings))
|
|
38
|
+
)
|
|
39
|
+
if has_errors(findings):
|
|
40
|
+
exit_code = EXIT_FAILED
|
|
41
|
+
finally:
|
|
42
|
+
reporter.close()
|
|
43
|
+
return exit_code
|
|
@@ -8,15 +8,18 @@ identical for both tiers and testable without a subprocess or a network.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import dataclasses
|
|
11
12
|
from collections.abc import Sequence
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
|
-
from ..domain.results import Fixture
|
|
15
|
+
from ..domain.results import Fixture, RunResult
|
|
15
16
|
from ..errors import PackError
|
|
16
17
|
from ..execution.executor import Executor
|
|
17
18
|
from ..packio.discovery import find_packs, resolve_pack_dir
|
|
18
19
|
from ..packio.loader import load_pack
|
|
19
20
|
from ..reporting.reporter import Reporter
|
|
21
|
+
from ..validation.registry import has_errors
|
|
22
|
+
from ..validation.registry import lint as run_rules
|
|
20
23
|
|
|
21
24
|
EXIT_OK = 0
|
|
22
25
|
EXIT_FAILED = 1
|
|
@@ -42,8 +45,25 @@ def run(
|
|
|
42
45
|
try:
|
|
43
46
|
for pack_dir in pack_dirs:
|
|
44
47
|
pack = load_pack(pack_dir)
|
|
48
|
+
|
|
49
|
+
# Static rules first. A pack whose testCases cannot be constructed
|
|
50
|
+
# produces an opaque traceback when executed, so there is nothing to
|
|
51
|
+
# learn from running it — report the real cause and move on.
|
|
52
|
+
findings = run_rules(pack)
|
|
53
|
+
if has_errors(findings):
|
|
54
|
+
reporter.report(
|
|
55
|
+
RunResult(pack_name=pack.name, executor="lint", diagnostics=tuple(findings))
|
|
56
|
+
)
|
|
57
|
+
exit_code = EXIT_FAILED
|
|
58
|
+
continue
|
|
59
|
+
|
|
45
60
|
chosen = list(fixtures) if fixtures else _default_fixtures(pack)
|
|
46
61
|
result = executor.execute(pack, chosen)
|
|
62
|
+
# Warnings from the static pass travel with the execution result so
|
|
63
|
+
# the author sees one report, not two.
|
|
64
|
+
result = dataclasses.replace(
|
|
65
|
+
result, diagnostics=tuple(findings) + tuple(result.diagnostics)
|
|
66
|
+
)
|
|
47
67
|
reporter.report(result)
|
|
48
68
|
# An inconclusive run means this tier lacked the infrastructure to
|
|
49
69
|
# judge the pack, not that the pack is wrong. Failing the command
|
|
@@ -61,12 +61,15 @@ class ApiClient:
|
|
|
61
61
|
|
|
62
62
|
def _headers(self, content_type: str | None = None) -> dict:
|
|
63
63
|
headers = {
|
|
64
|
-
"Authorization": f"Bearer {self._config.token}",
|
|
65
64
|
# Lets the server reject or warn on a CLI too old for the current
|
|
66
65
|
# pack contract, instead of failing somewhere obscure.
|
|
67
66
|
"X-Praxis-CLI-Version": __version__,
|
|
68
67
|
"Accept": "application/json",
|
|
69
68
|
}
|
|
69
|
+
# Public endpoints are called without credentials; sending an empty
|
|
70
|
+
# bearer token would be rejected as a malformed header.
|
|
71
|
+
if self._config.token:
|
|
72
|
+
headers["Authorization"] = f"Bearer {self._config.token}"
|
|
70
73
|
if content_type:
|
|
71
74
|
headers["Content-Type"] = content_type
|
|
72
75
|
return headers
|
|
@@ -67,6 +67,17 @@ class RemoteConfig:
|
|
|
67
67
|
#: so the CLI can show it before a publish. The server is authoritative.
|
|
68
68
|
company: str | None = None
|
|
69
69
|
|
|
70
|
+
@classmethod
|
|
71
|
+
def resolve_public(cls, api_url: str | None = None) -> RemoteConfig:
|
|
72
|
+
"""Config for endpoints that need no key.
|
|
73
|
+
|
|
74
|
+
Used by ``--example``, which someone runs before they have credentials;
|
|
75
|
+
requiring a login there would put a signup in front of the demo.
|
|
76
|
+
"""
|
|
77
|
+
stored = read_stored()
|
|
78
|
+
resolved_url = api_url or os.environ.get(ENV_API_URL) or stored.get("api_url") or DEFAULT_API_URL
|
|
79
|
+
return cls(api_url=resolved_url.rstrip("/"), token="", company=stored.get("company"))
|
|
80
|
+
|
|
70
81
|
@classmethod
|
|
71
82
|
def resolve(cls, api_url: str | None = None, token: str | None = None) -> RemoteConfig:
|
|
72
83
|
"""Build a config, or explain precisely what is missing."""
|
|
@@ -124,6 +124,16 @@ class HumanReporter:
|
|
|
124
124
|
style = self._style
|
|
125
125
|
seconds = result.duration_ms / 1000.0
|
|
126
126
|
|
|
127
|
+
# A static-only result has no fixtures, so the pass/fail rules below —
|
|
128
|
+
# which are about solution vs starter — do not apply. Its verdict is
|
|
129
|
+
# simply whether any rule reported an error.
|
|
130
|
+
if not result.runs:
|
|
131
|
+
if result.errors:
|
|
132
|
+
self._line(style.bad(" FAILED") + f" {len(result.errors)} problem(s) must be fixed")
|
|
133
|
+
else:
|
|
134
|
+
self._line(style.good(" OK") + style.dim(" no problems found"))
|
|
135
|
+
return
|
|
136
|
+
|
|
127
137
|
starter = result.run_for(Fixture.STARTER)
|
|
128
138
|
if starter is not None and starter.all_passed:
|
|
129
139
|
self._line(
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Generate a new pack from templates.
|
|
2
|
+
|
|
3
|
+
The generated pack is deliberately *complete and passing*: run
|
|
4
|
+
``codepraxis validate --local`` on it straight away and it goes green. That
|
|
5
|
+
gives an author a known-good starting point and proves their toolchain works
|
|
6
|
+
before they have written anything.
|
|
7
|
+
|
|
8
|
+
Templates ship inside the wheel and are read through ``importlib.resources`` so
|
|
9
|
+
this works from an installed package as well as a source checkout.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from importlib import resources
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..domain import contract
|
|
20
|
+
from ..errors import PraxisError
|
|
21
|
+
|
|
22
|
+
#: metadata.json "name" becomes a directory name inside the container, so it is
|
|
23
|
+
#: restricted to what a shell and a filesystem both handle without quoting.
|
|
24
|
+
_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]{2,49}$")
|
|
25
|
+
|
|
26
|
+
KNOWN_BACKENDS = ("AI", "DSA", "EMB", "LNX")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ScaffoldResult:
|
|
31
|
+
pack_dir: Path
|
|
32
|
+
solution_dir: Path
|
|
33
|
+
files: list
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_name(raw: str) -> str:
|
|
37
|
+
"""Turn a user-supplied slug into a valid pack name."""
|
|
38
|
+
name = raw.strip().lower().replace("-", "_").replace(" ", "_")
|
|
39
|
+
name = re.sub(r"_+", "_", name).strip("_")
|
|
40
|
+
if not _NAME_PATTERN.match(name):
|
|
41
|
+
raise PraxisError(
|
|
42
|
+
f"{raw!r} is not a usable pack name. Use 3-50 characters, lowercase "
|
|
43
|
+
f"letters, digits and underscores, starting with a letter."
|
|
44
|
+
)
|
|
45
|
+
return name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def title_from(name: str) -> str:
|
|
49
|
+
return name.replace("_", " ").strip().title()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _template(filename: str) -> str:
|
|
53
|
+
return (resources.files("codepraxis.scaffold") / "templates" / filename).read_text(encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _render(filename: str, name: str, backend: str, language: str) -> str:
|
|
57
|
+
return (
|
|
58
|
+
_template(filename)
|
|
59
|
+
.replace("__PACK_NAME__", name)
|
|
60
|
+
.replace("__TITLE__", title_from(name))
|
|
61
|
+
.replace("__BACKEND__", backend)
|
|
62
|
+
.replace("__LANGUAGE__", language)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create(
|
|
67
|
+
root: Path,
|
|
68
|
+
raw_name: str,
|
|
69
|
+
backend: str = "AI",
|
|
70
|
+
language: str = "PYTHON",
|
|
71
|
+
force: bool = False,
|
|
72
|
+
) -> ScaffoldResult:
|
|
73
|
+
"""Write a new pack under ``root`` and return what was created.
|
|
74
|
+
|
|
75
|
+
The pack directory and its ``solution/`` sibling are laid out the way the
|
|
76
|
+
tooling expects: the solution is *beside* the pack so it can never be
|
|
77
|
+
packaged and shipped to a candidate.
|
|
78
|
+
"""
|
|
79
|
+
name = normalize_name(raw_name)
|
|
80
|
+
backend = backend.strip().upper()
|
|
81
|
+
language = language.strip().upper()
|
|
82
|
+
|
|
83
|
+
if backend not in KNOWN_BACKENDS:
|
|
84
|
+
raise PraxisError(f"Unknown BACKEND {backend!r}. Expected one of: {', '.join(KNOWN_BACKENDS)}")
|
|
85
|
+
|
|
86
|
+
pack_dir = (root / name).resolve()
|
|
87
|
+
solution_dir = pack_dir.parent / "solution"
|
|
88
|
+
|
|
89
|
+
if pack_dir.exists() and not force:
|
|
90
|
+
raise PraxisError(f"{pack_dir} already exists. Choose another name, or pass --force.")
|
|
91
|
+
|
|
92
|
+
written = []
|
|
93
|
+
|
|
94
|
+
def write(path: Path, content: str) -> None:
|
|
95
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
path.write_text(content, encoding="utf-8")
|
|
97
|
+
written.append(path)
|
|
98
|
+
|
|
99
|
+
write(pack_dir / contract.METADATA_FILE, _render("metadata.json", name, backend, language))
|
|
100
|
+
write(pack_dir / contract.BACKEND_CONF_FILE, _render("backend.conf", name, backend, language))
|
|
101
|
+
write(pack_dir / contract.SOURCE_DIR / "main.py", _render("main.py", name, backend, language))
|
|
102
|
+
write(pack_dir / contract.SOURCE_DIR / "README.md", _render("README.md", name, backend, language))
|
|
103
|
+
write(pack_dir / contract.TESTS_DIR / "test_1.py", _render("test_1.py", name, backend, language))
|
|
104
|
+
write(
|
|
105
|
+
pack_dir / contract.COURSE_DATA_DIR / contract.COURSE_TOC_FILE,
|
|
106
|
+
_render("course_toc.json", name, backend, language),
|
|
107
|
+
)
|
|
108
|
+
write(pack_dir / contract.COURSE_DATA_DIR / "feature.md", _render("feature.md", name, backend, language))
|
|
109
|
+
|
|
110
|
+
# The reference solution overlays source/, so it mirrors those paths.
|
|
111
|
+
write(solution_dir / "main.py", _render("solution.py", name, backend, language))
|
|
112
|
+
|
|
113
|
+
return ScaffoldResult(pack_dir=pack_dir, solution_dir=solution_dir, files=written)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def describe(result: ScaffoldResult, root: Path) -> str:
|
|
117
|
+
def relative(path: Path) -> str:
|
|
118
|
+
try:
|
|
119
|
+
return str(path.relative_to(root))
|
|
120
|
+
except ValueError:
|
|
121
|
+
return str(path)
|
|
122
|
+
|
|
123
|
+
return "\n".join(
|
|
124
|
+
[
|
|
125
|
+
f"Created {relative(result.pack_dir)} ({len(result.files)} files)",
|
|
126
|
+
f" solution: {relative(result.solution_dir)} (beside the pack — never uploaded)",
|
|
127
|
+
"",
|
|
128
|
+
"It already validates. Check your setup:",
|
|
129
|
+
"",
|
|
130
|
+
f" codepraxis validate --local {result.pack_dir.name}",
|
|
131
|
+
"",
|
|
132
|
+
"Then make it your own:",
|
|
133
|
+
"",
|
|
134
|
+
" ._course_data/feature.md what the candidate reads",
|
|
135
|
+
" source/ what they start from",
|
|
136
|
+
" ._tests/test_1.py how it is graded",
|
|
137
|
+
" ../solution/ the reference answer",
|
|
138
|
+
]
|
|
139
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# __TITLE__
|
|
2
|
+
|
|
3
|
+
Replace this with the task the candidate has to do.
|
|
4
|
+
|
|
5
|
+
## What to build
|
|
6
|
+
|
|
7
|
+
`main.py` currently raises instead of returning a result. Make it work.
|
|
8
|
+
|
|
9
|
+
## Constraints
|
|
10
|
+
|
|
11
|
+
- Read arguments from the command line.
|
|
12
|
+
- Print one line to stdout and nothing else.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
Running with the arguments 1, 2 and 3 prints 6.
|
|
17
|
+
|
|
18
|
+
## Acceptance criteria
|
|
19
|
+
|
|
20
|
+
- Summing 1, 2 and 3 prints 6.
|
|
21
|
+
- Summing -5 and 5 prints 0.
|