solvewright 0.1.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.
Files changed (31) hide show
  1. solvewright/__init__.py +3 -0
  2. solvewright/__main__.py +3 -0
  3. solvewright/agents.py +450 -0
  4. solvewright/cli.py +263 -0
  5. solvewright/connection.py +137 -0
  6. solvewright/interviewer/cases/portals/brief.txt +13 -0
  7. solvewright/interviewer/cases/portals/interviewer.txt +49 -0
  8. solvewright/interviewer/cases/portals/reported-round.txt +34 -0
  9. solvewright/interviewer/data/portal-samples/sample-01.json +967 -0
  10. solvewright/interviewer/data/portal-samples/sample-02.json +967 -0
  11. solvewright/interviewer/data/portal-samples/sample-03.json +967 -0
  12. solvewright/interviewer/data/portal-samples/sample-04.json +967 -0
  13. solvewright/interviewer/data/portal-samples/sample-05.json +967 -0
  14. solvewright/interviewer/data/portal-samples/sample-06.json +967 -0
  15. solvewright/interviewer/data/portal-samples/sample-07.json +967 -0
  16. solvewright/interviewer/data/portal-samples/sample-08.json +967 -0
  17. solvewright/interviewer/interviewer.txt +113 -0
  18. solvewright/interviewer/references/case-generation.txt +52 -0
  19. solvewright/interviewer/references/curriculum.txt +21 -0
  20. solvewright/interviewer/references/scoring.txt +66 -0
  21. solvewright/interviewer/references/session-format.txt +60 -0
  22. solvewright/interviewer/tools/portal_lab.py +217 -0
  23. solvewright/runtime.py +673 -0
  24. solvewright/service.py +52 -0
  25. solvewright/settings.py +52 -0
  26. solvewright/worker.py +159 -0
  27. solvewright-0.1.0.dist-info/METADATA +132 -0
  28. solvewright-0.1.0.dist-info/RECORD +31 -0
  29. solvewright-0.1.0.dist-info/WHEEL +4 -0
  30. solvewright-0.1.0.dist-info/entry_points.txt +2 -0
  31. solvewright-0.1.0.dist-info/licenses/LICENSE +21 -0
solvewright/service.py ADDED
@@ -0,0 +1,52 @@
1
+ """HTTPS calls to Solvewright's account service. Nothing else leaves the machine."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import urllib.error
6
+ import urllib.request
7
+ from urllib.parse import urlparse
8
+
9
+ from . import __version__
10
+
11
+
12
+ class ServiceError(Exception):
13
+ """The service refused the request; retrying the same request will not help."""
14
+
15
+ def __init__(self, message: str, code: str | None = None):
16
+ super().__init__(message)
17
+ self.code = code
18
+
19
+
20
+ class Unavailable(OSError):
21
+ """The network or the service is down; the same request can be retried."""
22
+
23
+
24
+ class Service:
25
+ def __init__(self, url: str, key: str, timeout: float = 25):
26
+ parsed = urlparse(url)
27
+ local = parsed.scheme == "http" and parsed.hostname in ("127.0.0.1", "localhost")
28
+ if not key or not (parsed.scheme == "https" or local):
29
+ raise ValueError("The Solvewright service address must use HTTPS.")
30
+ self.url, self.key, self.timeout = url.rstrip("/"), key, timeout
31
+
32
+ def rpc(self, name: str, data: dict):
33
+ request = urllib.request.Request(
34
+ self.url + "/rest/v1/rpc/" + name,
35
+ data=json.dumps(data).encode("utf-8"),
36
+ headers={"apikey": self.key, "Content-Type": "application/json",
37
+ "User-Agent": f"solvewright-connector/{__version__}"},
38
+ )
39
+ try:
40
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
41
+ content = response.read()
42
+ return json.loads(content) if content else None
43
+ except urllib.error.HTTPError as error:
44
+ if error.code >= 500 or error.code in (408, 429):
45
+ raise Unavailable("Solvewright is temporarily unavailable.") from error
46
+ try:
47
+ detail = json.loads(error.read().decode("utf-8"))
48
+ except (ValueError, UnicodeError):
49
+ detail = {}
50
+ raise ServiceError(detail.get("message") or "Solvewright refused the request.", detail.get("code")) from error
51
+ except (urllib.error.URLError, TimeoutError, ConnectionError) as error:
52
+ raise Unavailable("Could not reach Solvewright. Check the internet connection.") from error
@@ -0,0 +1,52 @@
1
+ """Where the connector talks to, and where it keeps its files.
2
+
3
+ The service URL and key below are the same public values every visitor's
4
+ browser already receives from solvewright.app: a publishable key that can only
5
+ call the functions Solvewright exposes to anonymous clients. Row-level security
6
+ and the per-computer key decide what any request can reach.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ SITE_URL = "https://solvewright.app"
16
+ SERVICE_URL = "https://zqdxsgetwbeiwpjwjhzd.supabase.co"
17
+ SERVICE_KEY = "sb_publishable_zhQrE__vhJ1W6Nr-DSDXBQ_slqYpYUq"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Settings:
22
+ site_url: str
23
+ service_url: str
24
+ service_key: str
25
+ home: Path
26
+
27
+ @property
28
+ def connection_file(self) -> Path:
29
+ return self.home / "connection.json"
30
+
31
+ @property
32
+ def interviews(self) -> Path:
33
+ return self.home / "interviews"
34
+
35
+
36
+ def default_home() -> Path:
37
+ if sys.platform == "win32":
38
+ return Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local") / "Solvewright"
39
+ if sys.platform == "darwin":
40
+ return Path.home() / "Library" / "Application Support" / "Solvewright"
41
+ return Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") / "solvewright"
42
+
43
+
44
+ def load(home: str | os.PathLike[str] | None = None) -> Settings:
45
+ """Environment variables exist for development against a local stack."""
46
+ chosen = Path(home or os.environ.get("SOLVEWRIGHT_HOME") or default_home()).expanduser().resolve()
47
+ return Settings(
48
+ site_url=os.environ.get("SOLVEWRIGHT_SITE_URL", SITE_URL).rstrip("/"),
49
+ service_url=os.environ.get("SOLVEWRIGHT_SERVICE_URL", SERVICE_URL).rstrip("/"),
50
+ service_key=os.environ.get("SOLVEWRIGHT_SERVICE_KEY", SERVICE_KEY),
51
+ home=chosen,
52
+ )
solvewright/worker.py ADDED
@@ -0,0 +1,159 @@
1
+ """The job loop: fetch the account's requests over HTTPS, answer them, repeat.
2
+
3
+ This computer opens no port. Every request comes from the person's own signed-in
4
+ Solvewright account through the service, and every request is one of the fixed
5
+ operations below; there is no operation that runs an arbitrary command.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+ import time
12
+ import uuid
13
+
14
+ from . import __version__, agents
15
+ from .service import ServiceError, Unavailable
16
+
17
+ ROUND = re.compile(r"/sessions/([a-f0-9-]{36})(?:/(message|files|test-runs|interrupt))?")
18
+ ACTIVE_SECONDS, ACTIVE_POLL, IDLE_POLL = 180, 1.0, 4.0
19
+
20
+
21
+ class Revoked(Exception):
22
+ """This computer was disconnected from the account; its key no longer works."""
23
+
24
+
25
+ def health(runtime, models=False) -> dict:
26
+ described = agents.describe()
27
+ for name, info in described.items():
28
+ if info["available"]:
29
+ info["isolation"] = runtime.isolation.get(name, info["isolation"])
30
+ if models:
31
+ if described["claude"]["available"]:
32
+ described["claude"]["models"] = agents.CLAUDE_MODELS
33
+ described["claude"]["efforts"] = agents.CLAUDE_EFFORTS
34
+ if described["codex"]["available"]:
35
+ try:
36
+ described["codex"]["models"] = runtime.codex_models()
37
+ except (OSError, ValueError) as error:
38
+ described["codex"]["modelsError"] = str(error)[:300]
39
+ return {"version": 2, "connector": __version__, "agents": described,
40
+ # Kept for pages built before version 2.
41
+ "providers": {name: info["available"] for name, info in described.items()},
42
+ "dataDirectory": str(runtime.root)}
43
+
44
+
45
+ def dispatch(runtime, owner: str, path: str, body):
46
+ if body is not None and not isinstance(body, dict):
47
+ raise ValueError("Invalid interview request.")
48
+ if path == "/health" and body is None:
49
+ return health(runtime, models=True)
50
+ if path == "/sync":
51
+ if body is None:
52
+ return runtime.export_sync(owner)
53
+ if body.get("operation") == "manifest":
54
+ return runtime.export_sync(owner, metadata=True)
55
+ if body.get("operation") == "export":
56
+ if not isinstance(body.get("ids"), list):
57
+ raise ValueError("Choose which rounds to export.")
58
+ return runtime.export_sync(owner, ids=body["ids"])
59
+ if body.get("operation") is not None:
60
+ raise ValueError("Unknown account history operation.")
61
+ return runtime.import_sync(owner, body)
62
+ if path == "/sessions":
63
+ return {"sessions": runtime.sessions(owner)} if body is None else runtime.create(owner, body)
64
+ match = ROUND.fullmatch(path)
65
+ if not match:
66
+ raise ValueError("Unknown interview operation.")
67
+ session_id, operation = match.groups()
68
+ if body is None and operation is None:
69
+ return runtime.public(owner, runtime.load(owner, session_id), True)
70
+ if body is None:
71
+ raise ValueError("This interview operation needs a request body.")
72
+ if operation == "message":
73
+ message = body.get("message", "")
74
+ if not isinstance(message, str) or not message.strip() or len(message) > 30_000:
75
+ raise ValueError("Send a message of at most 30,000 characters.")
76
+ runtime.start_turn(owner, session_id, message, remark=body.get("remark") is True)
77
+ return {"ok": True}
78
+ if operation == "files":
79
+ runtime.save_files(owner, session_id, body.get("files"))
80
+ return {"ok": True}
81
+ if operation == "test-runs":
82
+ return runtime.record_test_run(owner, session_id, body.get("files"), body.get("run"))
83
+ if operation == "interrupt":
84
+ runtime.interrupt(owner, session_id)
85
+ return {"ok": True}
86
+ raise ValueError("Unsupported interview operation.")
87
+
88
+
89
+ def describe_job(path: str, body) -> str | None:
90
+ """One line for the terminal, without the request's content."""
91
+ if path == "/sessions" and body is not None:
92
+ return None # The runtime announces the new round itself.
93
+ match = ROUND.fullmatch(path)
94
+ operation = match.group(2) if match else None
95
+ return {"files": "Saved your code", "interrupt": "Stopped the interviewer"}.get(operation)
96
+
97
+
98
+ def details(runtime) -> dict:
99
+ return {"connector": __version__, "agents": {name: {"version": info["version"], "isolation": info["isolation"]}
100
+ for name, info in health(runtime)["agents"].items() if info["available"]}}
101
+
102
+
103
+ def run(runtime, service, credentials: dict, say=print, sleep=time.sleep, clock=time.monotonic) -> None:
104
+ identity = {"p_worker_id": credentials["workerId"], "p_key": credentials["key"], "p_instance_id": str(uuid.uuid4())}
105
+ last_job = clock()
106
+ announced = None
107
+ started = clock()
108
+ try:
109
+ while True:
110
+ try:
111
+ jobs = service.rpc("next_interview_jobs", {**identity, "p_details": details(runtime)})
112
+ except Unavailable:
113
+ if announced != "offline":
114
+ say("Connection interrupted; retrying until Solvewright answers…")
115
+ announced = "offline"
116
+ sleep(3)
117
+ continue
118
+ except ServiceError as error:
119
+ if error.code == "42501":
120
+ raise Revoked() from error
121
+ # A connector that was killed rather than closed still counts as running for 30 s.
122
+ if error.code == "PT409" and clock() - started < 45:
123
+ if announced != "waiting":
124
+ say("An earlier connector on this computer is still marked as running; waiting for it to time out…")
125
+ announced = "waiting"
126
+ sleep(5)
127
+ continue
128
+ raise
129
+ if announced in ("offline", "waiting"):
130
+ say("Reconnected.")
131
+ announced = "online"
132
+ for job in jobs or []:
133
+ last_job = clock()
134
+ result, failure = None, None
135
+ try:
136
+ result = dispatch(runtime, credentials["owner"], job["path"], job.get("body"))
137
+ line = describe_job(job["path"], job.get("body"))
138
+ if line:
139
+ say(line)
140
+ except (ValueError, KeyError, TypeError, OSError) as error:
141
+ failure = str(error) or "The request failed."
142
+ # Retrying delivery never repeats the operation itself.
143
+ while True:
144
+ try:
145
+ service.rpc("complete_interview_job", {**identity, "p_id": job["id"], "p_result": result, "p_error": failure})
146
+ break
147
+ except Unavailable:
148
+ sleep(3)
149
+ if not jobs:
150
+ sleep(ACTIVE_POLL if clock() - last_job < ACTIVE_SECONDS else IDLE_POLL)
151
+ finally:
152
+ try:
153
+ service.rpc("disconnect_interview_worker", identity)
154
+ except (OSError, ServiceError):
155
+ pass
156
+
157
+
158
+ def stderr(text: str) -> None:
159
+ print(text, file=sys.stderr, flush=True)
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.5
2
+ Name: solvewright
3
+ Version: 0.1.0
4
+ Summary: Connect your own Claude Code or Codex to Solvewright Interview Lab for mock technical interviews.
5
+ Project-URL: Homepage, https://solvewright.app/coding/interviews
6
+ Project-URL: Source, https://github.com/fus3r/solvewright-connector
7
+ Project-URL: Privacy, https://solvewright.app/privacy#privacy-interviews
8
+ Author: Riad Darwish
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude code,codex,interview,mock interview,practice
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Education
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Solvewright connector
25
+
26
+ Practise live technical interviews in [Solvewright Interview Lab](https://solvewright.app/coding/interviews)
27
+ with the Claude Code or Codex you already use. The interviewer runs on your computer, under your own
28
+ sign-in and plan. Solvewright provides the room: the problem, the editor, the tests, the conversation,
29
+ your history and your skill profile.
30
+
31
+ ```sh
32
+ uvx solvewright
33
+ ```
34
+
35
+ No uv? `pipx run solvewright` works the same way, or `python3 -m pip install --user solvewright` and
36
+ then `solvewright`. Python 3.10 or newer. The package has no dependencies.
37
+
38
+ ## What happens when you run it
39
+
40
+ 1. The terminal lists this computer, the agents it found and where interviews are kept.
41
+ 2. It prints a short code and opens `solvewright.app/connect`. Check that the page shows the same
42
+ code, then press **Connect this computer**. You must be signed in to Solvewright.
43
+ 3. The terminal says which account it joined. Keep the window open while you practise; it prints one
44
+ line for each thing it does (a round prepared, a reply, tests recorded).
45
+ 4. Press Ctrl+C to stop. Nothing keeps running in the background, and nothing starts again by itself.
46
+
47
+ Next time, `solvewright` reconnects without a new code. `solvewright disconnect` removes this
48
+ computer from your account; you can also remove it from Interview Lab, which stops its key at once.
49
+
50
+ | Command | Does |
51
+ | --- | --- |
52
+ | `solvewright` | Connect if needed, then wait for rounds |
53
+ | `solvewright connect` | Approve this computer again, with a new key |
54
+ | `solvewright status` | Show the connection and the agents found here |
55
+ | `solvewright disconnect` | Remove this computer from your account |
56
+ | `solvewright doctor` | Check Python, Claude Code, Codex and the sandbox |
57
+
58
+ ## What it can and cannot do
59
+
60
+ The connector is deliberately small. Every request it accepts is one of a fixed list: report its
61
+ status, start a round, pass your message to the interviewer, save your files, record a test result,
62
+ stop the interviewer, and sync your history. There is no request that runs a command.
63
+
64
+ - **It opens no port.** It connects out to Solvewright over HTTPS and asks for work.
65
+ - **It only takes requests from your account.** This computer's key is issued after you approve it
66
+ in the browser, is stored only here (`connection.json`, readable by you alone), and the service
67
+ keeps a hash of it.
68
+ - **It never sees your Claude or ChatGPT credentials.** It starts the official `claude` or `codex`
69
+ program, unmodified, which signs in the way it always does. Usage counts against your own plan.
70
+ - **It never runs your code.** Public tests run in your browser, in Pyodide (Python compiled to
71
+ WebAssembly). The connector only stores the files you tested and the result.
72
+ - **It does not update itself.** What runs is the version you installed from PyPI. The interviewer's
73
+ instructions ship inside the package (`src/solvewright/interviewer/`), so you can read exactly what
74
+ your agent is told.
75
+
76
+ ### The interviewer's sandbox
77
+
78
+ The interviewer writes the problem and checks its own reference solution, so it needs to run code.
79
+ It does that inside its own agent's sandbox, confined to the folder of the current round:
80
+
81
+ | | Commands | Files | Network |
82
+ | --- | --- | --- | --- |
83
+ | Claude Code, macOS and Linux | Claude Code's OS sandbox | Read and write only in the round folder; the rest of your home folder is unreadable | None |
84
+ | Codex, macOS and Linux | Codex's sandbox with a Solvewright permission profile | Read and write in your interview folders; the rest of your home folder is unreadable | None |
85
+ | Claude Code, native Windows | No command tool at all: its sandbox needs macOS, Linux or WSL2 | The file tools stay in the round folder | None |
86
+
87
+ Claude Code runs with `--restricted --safe-mode --strict-mcp-config`: your own CLAUDE.md, hooks,
88
+ plugins and MCP servers are not loaded. Codex runs through its documented app-server with its plugins,
89
+ apps, hooks, web search, memories and every MCP server you configured switched off for the interview.
90
+ If a platform cannot provide one of these limits, the connector removes the capability rather than the
91
+ limit, and says so in the terminal and in Interview Lab. On Linux, Claude Code's sandbox needs
92
+ `bubblewrap` and `socat`.
93
+
94
+ ## What is stored where
95
+
96
+ On this computer, in one folder (`solvewright status` prints it):
97
+
98
+ | System | Folder |
99
+ | --- | --- |
100
+ | macOS | `~/Library/Application Support/Solvewright` |
101
+ | Windows | `%LOCALAPPDATA%\Solvewright` |
102
+ | Linux | `$XDG_DATA_HOME/solvewright`, by default `~/.local/share/solvewright` |
103
+
104
+ Each round is a folder holding the candidate files, the interviewer's private files, snapshots of
105
+ what you submitted, and the conversation. When you are signed in, Interview Lab also keeps a copy in
106
+ your Solvewright account so you can continue on another computer; deleting your account deletes it.
107
+ Your agent sends what it reads to its own provider (Anthropic or OpenAI) under your account's terms,
108
+ as it does in any other session. Details: [solvewright.app/privacy](https://solvewright.app/privacy#privacy-interviews).
109
+
110
+ ## Read the code
111
+
112
+ About 1,800 lines of Python in `src/solvewright/`:
113
+
114
+ | File | Role |
115
+ | --- | --- |
116
+ | `cli.py` | The commands and everything the terminal prints |
117
+ | `connection.py` | The browser approval and the saved key |
118
+ | `service.py` | The only network calls: HTTPS to Solvewright's account service |
119
+ | `worker.py` | The fixed list of requests and the loop that fetches them |
120
+ | `agents.py` | How Claude Code and Codex are started, and their sandbox settings |
121
+ | `runtime.py` | Rounds on disk, account-history sync, the interviewer's turns |
122
+ | `interviewer/` | The instructions and references the interviewer reads |
123
+
124
+ The account service address and its publishable key are in `settings.py`. They are the same public
125
+ values every visitor's browser receives from solvewright.app.
126
+
127
+ Run the tests from this folder with `PYTHONPATH=src python -m unittest discover -s tests` (on Windows, set `PYTHONPATH=src` first).
128
+
129
+ ## Licence
130
+
131
+ MIT. Solvewright is not affiliated with Anthropic or OpenAI. Claude Code and Codex are their
132
+ respective owners' products; this program runs the versions you installed.
@@ -0,0 +1,31 @@
1
+ solvewright/__init__.py,sha256=2AO3NySPLVN1hawJTBPPdzm8teKtYdHkCpZ2PTSjWJk,106
2
+ solvewright/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ solvewright/agents.py,sha256=xw1efno_Dxm7JcZQH99HWpY18zfZ2w2HmJVrl7cWgSU,21959
4
+ solvewright/cli.py,sha256=7TS9PDeAA9yDCzzMFKbuM6ZkqkbI3btGsuD5tPds-sc,11234
5
+ solvewright/connection.py,sha256=93GzPwVSBkPGusDkEj-XkYsSXeRDvLPcx2z_3Tjjbcg,4626
6
+ solvewright/runtime.py,sha256=UwPJqsR-DpIOuO6iGpyINuLQovXiTgY3Ulf2SgAnbLA,42397
7
+ solvewright/service.py,sha256=YZdug-euCS80lk0OvDOc7JSSukjlTtozp4To0JNTc8Y,2216
8
+ solvewright/settings.py,sha256=N2DVOloCRduej-b81Jui-X0i7kgoKRq9qYxHwTUCPlM,1858
9
+ solvewright/worker.py,sha256=cN6WwazsiqkzAYKVhJsc6X7HBgxnS7zrngTJwzg03m4,7102
10
+ solvewright/interviewer/interviewer.txt,sha256=TwL0Mel-PW2VXZYue9T36v7y-tS2x48x-Q-rJJ7rDSA,13747
11
+ solvewright/interviewer/cases/portals/brief.txt,sha256=iD_Hrh8pAuzTR62I_W56KaP_CcgruW_WZrOPdyDp_Ok,1869
12
+ solvewright/interviewer/cases/portals/interviewer.txt,sha256=EeApJ2ZgIG2vlmjWzaxqT2KSSn4XiLfu9gns68yGwmM,6834
13
+ solvewright/interviewer/cases/portals/reported-round.txt,sha256=XRG1kiJVFlKBIPHtnf9MT2cUFMTJzh4gbDq4d0R11fQ,3290
14
+ solvewright/interviewer/data/portal-samples/sample-01.json,sha256=8CEaVoZhRuPW9b0tAJpBoR5KAcDTvLVj6mPAUlmMbiE,13857
15
+ solvewright/interviewer/data/portal-samples/sample-02.json,sha256=2EzFEoI3kC6cppWJiFu8mOcGOzxlEuIj8lqsYeIalLg,13737
16
+ solvewright/interviewer/data/portal-samples/sample-03.json,sha256=0Mj28_Esd-ktq3BG28SGfDeWsS8dbiY-S7DyCf5ocSc,13495
17
+ solvewright/interviewer/data/portal-samples/sample-04.json,sha256=nYFy0bwGlIEQuk1_4Y2neanKapdXoMODcU1T2q8OoLw,13987
18
+ solvewright/interviewer/data/portal-samples/sample-05.json,sha256=5_MD8LVX3REfffkYdol1m4uatqQ3Jesl0-st5NcA-fg,13893
19
+ solvewright/interviewer/data/portal-samples/sample-06.json,sha256=IFUf2ZgtUtPHAJlpB_0s9otNOfHCNOjpmBCL7RcwME4,13782
20
+ solvewright/interviewer/data/portal-samples/sample-07.json,sha256=P636q5FmaBqDz3GMplO1vfJxH3no4meQORyDpfAw9FE,13565
21
+ solvewright/interviewer/data/portal-samples/sample-08.json,sha256=jefyq-F7LaMFiPxMMSS7pWH8sBnISLSNfhTQb7IGK2Q,13643
22
+ solvewright/interviewer/references/case-generation.txt,sha256=PuFv0T0G99ZcxHGN0YbBKEKJOyNhqMNgAfuH7sbdM74,7808
23
+ solvewright/interviewer/references/curriculum.txt,sha256=17ch-6p0R4ok3YUZSRPSZR7-QIhCJWMo_dp7JwtfFGI,3112
24
+ solvewright/interviewer/references/scoring.txt,sha256=hyiw_XY3r8bzQhfhwnCHfYlyeSMmdRwlg-NgIzB6jNc,6438
25
+ solvewright/interviewer/references/session-format.txt,sha256=DRAdjNWNnzOQgbNIwpjdIbkh0gYQ9HapjGpT4dGK1g8,4528
26
+ solvewright/interviewer/tools/portal_lab.py,sha256=c7lNDFMvZVi7KRXv42zyP5Sc5llvqsnsUb9tQDMf-xc,9088
27
+ solvewright-0.1.0.dist-info/METADATA,sha256=A3Ruj_abXbEqVOWRahRCD37Whw-ZEEqMxfod9O1RZCQ,7129
28
+ solvewright-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
29
+ solvewright-0.1.0.dist-info/entry_points.txt,sha256=YL71CWedKPiXyPioNc3tYor2wo38z_wQN0JU9VTcTU8,53
30
+ solvewright-0.1.0.dist-info/licenses/LICENSE,sha256=FtM09nZcmb027gl4INBvq59AZFWKrCdO9D-dwwUKufg,1069
31
+ solvewright-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ solvewright = solvewright.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riad Darwish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.