roadmodel 0.2.82__tar.gz → 0.2.83__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 (72) hide show
  1. {roadmodel-0.2.82 → roadmodel-0.2.83}/PKG-INFO +1 -1
  2. {roadmodel-0.2.82 → roadmodel-0.2.83}/pyproject.toml +1 -1
  3. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/update_projects.py +132 -29
  4. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/__init__.py +1 -1
  5. {roadmodel-0.2.82 → roadmodel-0.2.83}/.github/README.md +0 -0
  6. {roadmodel-0.2.82 → roadmodel-0.2.83}/.gitignore +0 -0
  7. {roadmodel-0.2.82 → roadmodel-0.2.83}/LICENSE +0 -0
  8. {roadmodel-0.2.82 → roadmodel-0.2.83}/NOTICE +0 -0
  9. {roadmodel-0.2.82 → roadmodel-0.2.83}/README.md +0 -0
  10. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/benchmarks.json +0 -0
  11. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/catalog.json +0 -0
  12. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/claude-commands/roadmap-phase.md +0 -0
  13. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/claude-commands/roadmap-project.md +0 -0
  14. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/claude-commands/roadmap-refresh.md +0 -0
  15. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/claude-commands/roadmap-step.md +0 -0
  16. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/claude-commands/roadmodel-upgrade.md +0 -0
  17. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/context-windows.json +0 -0
  18. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/effort-tokens.json +0 -0
  19. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/model-selector.txt +0 -0
  20. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/model-tier-cost-scale.md +0 -0
  21. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/settings-display.md +0 -0
  22. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/templates/phase-roadmap-template.md +0 -0
  23. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/templates/planning-kit-how-to-use.md +0 -0
  24. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/templates/project-roadmap-template.md +0 -0
  25. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/templates/prompt-phase-roadmap.md +0 -0
  26. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/templates/prompt-project-roadmap.md +0 -0
  27. {roadmodel-0.2.82 → roadmodel-0.2.83}/docs/user-context.example.md +0 -0
  28. {roadmodel-0.2.82 → roadmodel-0.2.83}/hatch_build.py +0 -0
  29. {roadmodel-0.2.82 → roadmodel-0.2.83}/infra/README.md +0 -0
  30. {roadmodel-0.2.82 → roadmodel-0.2.83}/infra/gcp-killswitch/README.md +0 -0
  31. {roadmodel-0.2.82 → roadmodel-0.2.83}/infra/supabase/README.md +0 -0
  32. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/benchmarks.json +0 -0
  33. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/catalog.json +0 -0
  34. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/context-windows.json +0 -0
  35. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/effort-tokens.json +0 -0
  36. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/model-selector.txt +0 -0
  37. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/model-tier-cost-scale.md +0 -0
  38. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/phase-roadmap-template.md +0 -0
  39. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/planning-kit-how-to-use.md +0 -0
  40. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/project-roadmap-template.md +0 -0
  41. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/prompt-phase-roadmap.md +0 -0
  42. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/prompt-project-roadmap.md +0 -0
  43. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/roadmap-phase.md +0 -0
  44. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/roadmap-project.md +0 -0
  45. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/roadmap-refresh.md +0 -0
  46. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/roadmap-step.md +0 -0
  47. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/roadmodel-upgrade.md +0 -0
  48. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/settings-display.md +0 -0
  49. {roadmodel-0.2.82 → roadmodel-0.2.83}/roadmodel/data/user-context.example.md +0 -0
  50. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/agent_context.py +0 -0
  51. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/agent_rules.py +0 -0
  52. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/updater_lock.py +0 -0
  53. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/upgrade_fleet.py +0 -0
  54. {roadmodel-0.2.82 → roadmodel-0.2.83}/scripts/worktree_memory.py +0 -0
  55. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/__main__.py +0 -0
  56. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/cli.py +0 -0
  57. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/config.py +0 -0
  58. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/cost.py +0 -0
  59. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/errors.py +0 -0
  60. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/mcp_server.py +0 -0
  61. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/private_files.py +0 -0
  62. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/__init__.py +0 -0
  63. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/_safety.py +0 -0
  64. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/anthropic.py +0 -0
  65. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/google.py +0 -0
  66. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/openai.py +0 -0
  67. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/openai_compatible.py +0 -0
  68. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/providers/registry.py +0 -0
  69. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/recommend.py +0 -0
  70. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/scoring.py +0 -0
  71. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/usage.py +0 -0
  72. {roadmodel-0.2.82 → roadmodel-0.2.83}/src/roadmodel/user_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: roadmodel
3
- Version: 0.2.82
3
+ Version: 0.2.83
4
4
  Summary: BYO-key CLI that recommends the right AI model, platform, and settings for a prompt.
5
5
  Project-URL: Homepage, https://roadmodel.ai
6
6
  Project-URL: Repository, https://github.com/nathanramoscfa/roadmodel
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "roadmodel"
7
- version = "0.2.82"
7
+ version = "0.2.83"
8
8
  description = "BYO-key CLI that recommends the right AI model, platform, and settings for a prompt."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -49,6 +49,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
49
49
  from dataclasses import dataclass, field
50
50
  from pathlib import Path
51
51
  from typing import Optional
52
+ from urllib.parse import urlsplit
52
53
  from xml.sax.saxutils import escape
53
54
 
54
55
  COMMANDS = (
@@ -1975,7 +1976,8 @@ def _parse_time(value: str) -> tuple[int, int]:
1975
1976
  def launchd_plist(python: Path, script: Path, hour: int, minute: int, log: Path) -> str:
1976
1977
  # stdout goes nowhere: `--log` already writes the run to the log, and
1977
1978
  # launchd capturing stdout into the same file recorded every run twice.
1978
- # stderr stays captured, so a crash before the log opens still lands there.
1979
+ # --log also captures stderr after its safe open. launchd must not pre-open
1980
+ # a path outside that validation; failures before log setup are discarded.
1979
1981
  return f"""<?xml version="1.0" encoding="UTF-8"?>
1980
1982
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1981
1983
  <plist version="1.0">
@@ -1994,7 +1996,7 @@ def launchd_plist(python: Path, script: Path, hour: int, minute: int, log: Path)
1994
1996
  <dict><key>Hour</key><integer>{hour}</integer><key>Minute</key><integer>{minute}</integer></dict>
1995
1997
  <key>RunAtLoad</key><false/>
1996
1998
  <key>StandardOutPath</key><string>/dev/null</string>
1997
- <key>StandardErrorPath</key><string>{escape(str(log))}</string>
1999
+ <key>StandardErrorPath</key><string>/dev/null</string>
1998
2000
  <key>EnvironmentVariables</key>
1999
2001
  <dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
2000
2002
  </dict>
@@ -2248,7 +2250,90 @@ CONTEXT_SYNC_CONFIG = CONFIG_DIR / "context-sync.json"
2248
2250
  CONTEXT_CLONE = CONFIG_DIR / "context-repo"
2249
2251
  USER_CONTEXT = CONFIG_DIR / "user-context.md"
2250
2252
  CONTEXT_FILE = "user-context.md"
2251
- _REPO_SLUG_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
2253
+ _REPO_SLUG_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9-]{0,38}/[A-Za-z0-9_][A-Za-z0-9_.-]{0,99}")
2254
+
2255
+
2256
+ def _context_repo(value: str) -> str:
2257
+ """One GitHub repository identity, with no local paths or URL credentials."""
2258
+ if not isinstance(value, str) or any(ord(c) < 33 or ord(c) == 127 for c in value):
2259
+ raise ValueError("Unsupported context repository.")
2260
+ candidate = value
2261
+ if value.startswith("git@github.com:"):
2262
+ candidate = value[len("git@github.com:") :]
2263
+ elif "://" in value:
2264
+ parsed = urlsplit(value)
2265
+ if (
2266
+ parsed.hostname != "github.com"
2267
+ or parsed.port is not None
2268
+ or parsed.query
2269
+ or parsed.fragment
2270
+ ):
2271
+ raise ValueError("Unsupported context repository.")
2272
+ if parsed.scheme == "https":
2273
+ if parsed.username is not None or parsed.password is not None:
2274
+ raise ValueError("Unsupported context repository.")
2275
+ elif parsed.scheme == "ssh":
2276
+ if parsed.username != "git" or parsed.password is not None:
2277
+ raise ValueError("Unsupported context repository.")
2278
+ else:
2279
+ raise ValueError("Unsupported context repository.")
2280
+ candidate = parsed.path.removeprefix("/")
2281
+ if candidate.endswith(".git"):
2282
+ candidate = candidate[:-4]
2283
+ if not _REPO_SLUG_RE.fullmatch(candidate):
2284
+ raise ValueError("Unsupported context repository.")
2285
+ return candidate.lower()
2286
+
2287
+
2288
+ def _context_visibility(repo: str) -> bool:
2289
+ """Require authenticated, exact-identity PRIVATE metadata before carrying bytes."""
2290
+ try:
2291
+ gh = shutil.which("gh")
2292
+ if not gh:
2293
+ return False
2294
+ result = subprocess.run( # noqa: S603 — fixed gh API, canonical repository
2295
+ [gh, "api", "--hostname", "github.com", f"repos/{repo}"],
2296
+ capture_output=True,
2297
+ text=True,
2298
+ timeout=30,
2299
+ )
2300
+ if result.returncode != 0:
2301
+ return False
2302
+ data = json.loads(result.stdout)
2303
+ return (
2304
+ isinstance(data, dict)
2305
+ and data.get("private") is True
2306
+ and data.get("visibility") == "private"
2307
+ and isinstance(data.get("full_name"), str)
2308
+ and data["full_name"].lower() == repo
2309
+ )
2310
+ except (OSError, ValueError, subprocess.SubprocessError):
2311
+ return False
2312
+
2313
+
2314
+ def _context_destination(repo: str) -> Optional[str]:
2315
+ """Validate effective Git URL rewrites, configured transports and current branch."""
2316
+ for args in (
2317
+ ("remote", "get-url", "--all", "origin"),
2318
+ ("remote", "get-url", "--push", "--all", "origin"),
2319
+ ):
2320
+ result = _git(*args, cwd=CONTEXT_CLONE)
2321
+ values = result.stdout.splitlines()
2322
+ if result.returncode or len(values) != 1 or _context_repo(values[0]) != repo:
2323
+ return None
2324
+ result = _git("symbolic-ref", "--quiet", "--short", "HEAD", cwd=CONTEXT_CLONE)
2325
+ branch = result.stdout.strip()
2326
+ if result.returncode or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", branch):
2327
+ return None
2328
+ if _git("check-ref-format", "--branch", branch, cwd=CONTEXT_CLONE).returncode:
2329
+ return None
2330
+ for name in (f"branch.{branch}.remote", f"branch.{branch}.pushRemote", "remote.pushDefault"):
2331
+ result = _git("config", "--get-all", name, cwd=CONTEXT_CLONE)
2332
+ if result.returncode not in (0, 1) or any(
2333
+ v != "origin" for v in result.stdout.splitlines()
2334
+ ):
2335
+ return None
2336
+ return branch
2252
2337
 
2253
2338
 
2254
2339
  def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess[str]:
@@ -2270,42 +2355,37 @@ def _read_context_config() -> Optional[dict[str, str]]:
2270
2355
 
2271
2356
  def configure_context_sync(repo: str, source: Optional[str], dry_run: bool = False) -> str:
2272
2357
  """Record this machine's role. `source` given → it publishes that file;
2273
- omitted → it pulls. `repo` is `owner/name` (or a git URL / path)."""
2274
- if not (_REPO_SLUG_RE.match(repo) or "://" in repo or repo.startswith(("git@", "/"))):
2275
- raise SystemExit(f"--context-sync: expected OWNER/REPO, got {repo!r}")
2358
+ omitted → it pulls. `repo` identifies a GitHub repository."""
2359
+ try:
2360
+ repo = _context_repo(repo)
2361
+ except ValueError:
2362
+ raise SystemExit("--context-sync: expected a canonical GitHub repository.") from None
2276
2363
  cfg: dict[str, str] = {"repo": repo, "role": "replica"}
2277
2364
  if source:
2278
2365
  src = Path(source).expanduser().resolve()
2279
2366
  if not src.is_file():
2280
- raise SystemExit(f"--context-source: no such file {src}")
2367
+ raise SystemExit("--context-source: source file is unavailable.")
2281
2368
  cfg.update(role="source", source=str(src))
2282
2369
  if not dry_run:
2283
2370
  _private_dir(CONFIG_DIR)
2284
2371
  _replace_bytes(CONTEXT_SYNC_CONFIG, (json.dumps(cfg, indent=2) + "\n").encode())
2285
- where = f"publishing {cfg['source']}" if source else f"pulling into {USER_CONTEXT}"
2286
- return f"this machine is the user-context {cfg['role'].upper()}, {where}, via {repo}"
2372
+ return f"this machine is the user-context {cfg['role'].upper()}; privacy verification required at each sync"
2287
2373
 
2288
2374
 
2289
2375
  def _ensure_clone(repo: str) -> Optional[str]:
2290
- """None on success, else why it failed. A slug clones through `gh` (which
2291
- carries the operator's GitHub auth for a PRIVATE repo); a URL or path
2292
- clones with plain git."""
2376
+ """Clone only the independently verified GitHub repository through gh."""
2293
2377
  if (CONTEXT_CLONE / ".git").is_dir():
2294
2378
  _private_dir(CONTEXT_CLONE)
2295
2379
  return None
2296
2380
  _private_dir(CONTEXT_CLONE.parent)
2297
- if _REPO_SLUG_RE.match(repo) and shutil.which("gh"):
2298
- cmd = ["gh", "repo", "clone", repo, str(CONTEXT_CLONE)]
2299
- else:
2300
- url = f"https://github.com/{repo}.git" if _REPO_SLUG_RE.match(repo) else repo
2301
- cmd = ["git", "clone", url, str(CONTEXT_CLONE)]
2381
+ cmd = ["gh", "repo", "clone", repo, str(CONTEXT_CLONE)]
2302
2382
  done = subprocess.run( # noqa: S603 — literal argv built above from a validated repo, no shell
2303
2383
  cmd, capture_output=True, text=True, timeout=180
2304
2384
  )
2305
2385
  if done.returncode == 0:
2306
2386
  _private_dir(CONTEXT_CLONE)
2307
2387
  return None
2308
- return _tail(done.stderr or done.stdout, 2)
2388
+ return "clone failed"
2309
2389
 
2310
2390
 
2311
2391
  def _digest(data: bytes) -> str:
@@ -2318,33 +2398,49 @@ def sync_user_context(dry_run: bool = False) -> list[str]:
2318
2398
  """Publish (source) or pull (replica) the user-context. Silent when this
2319
2399
  machine was never configured, so an operator with one machine sees
2320
2400
  nothing new."""
2401
+ try:
2402
+ return _sync_user_context(dry_run)
2403
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError):
2404
+ return ["user-context sync: SKIPPED (private destination could not be verified)"]
2405
+
2406
+
2407
+ def _sync_user_context(dry_run: bool) -> list[str]:
2321
2408
  cfg = _read_context_config()
2322
2409
  if cfg is None:
2323
2410
  return []
2324
2411
  if dry_run:
2325
- return [f"user-context sync: {cfg['role']} via {cfg['repo']} (dry run: nothing sent)"]
2326
- failed = _ensure_clone(cfg["repo"])
2412
+ return ["user-context sync: dry run; privacy unverified, nothing sent"]
2413
+ repo = _context_repo(cfg["repo"])
2414
+ if not _context_visibility(repo):
2415
+ return ["user-context sync: SKIPPED (private destination could not be verified)"]
2416
+ if CONTEXT_CLONE.exists() and (
2417
+ not (CONTEXT_CLONE / ".git").is_dir() or not _context_destination(repo)
2418
+ ):
2419
+ return ["user-context sync: SKIPPED (clone destination could not be verified)"]
2420
+ failed = _ensure_clone(repo)
2327
2421
  if failed:
2328
- return [f"user-context sync: SKIPPED (clone of {cfg['repo']} failed: {failed})"]
2422
+ return ["user-context sync: SKIPPED (clone failed)"]
2423
+ branch = _context_destination(repo)
2424
+ if branch is None:
2425
+ return ["user-context sync: SKIPPED (clone destination could not be verified)"]
2329
2426
  has_history = _git("rev-parse", "--verify", "HEAD", cwd=CONTEXT_CLONE).returncode == 0
2330
2427
  if has_history:
2331
- pulled = _git("pull", "--ff-only", "--quiet", cwd=CONTEXT_CLONE)
2428
+ pulled = _git("pull", "--ff-only", "--quiet", "origin", branch, cwd=CONTEXT_CLONE)
2332
2429
  if pulled.returncode != 0:
2333
- return [f"user-context sync: SKIPPED (pull failed: {_tail(pulled.stderr, 2)})"]
2430
+ return ["user-context sync: SKIPPED (pull failed)"]
2334
2431
  carried = CONTEXT_CLONE / CONTEXT_FILE
2335
2432
 
2336
2433
  if cfg["role"] == "source":
2337
2434
  src = Path(cfg["source"])
2338
2435
  if not src.is_file():
2339
- return [f"user-context sync: SKIPPED (source {src} is missing)"]
2436
+ return ["user-context sync: SKIPPED (source file is unavailable)"]
2340
2437
  out: list[str] = []
2341
2438
  # An edit to a separate copy at the default path would never publish.
2342
2439
  if USER_CONTEXT.exists() and not USER_CONTEXT.is_symlink():
2343
2440
  if USER_CONTEXT.read_bytes() != src.read_bytes():
2344
2441
  out.append(
2345
- f"user-context sync: WARNING {USER_CONTEXT} is a separate copy that "
2346
- f"differs from the source; edits there are NOT published. Point it at "
2347
- f"{src}."
2442
+ "user-context sync: WARNING default context differs from the source; "
2443
+ "edits to that separate copy are NOT published."
2348
2444
  )
2349
2445
  data = src.read_bytes()
2350
2446
  if carried.exists() and carried.read_bytes() == data:
@@ -2354,14 +2450,21 @@ def sync_user_context(dry_run: bool = False) -> list[str]:
2354
2450
  steps = (
2355
2451
  ("add", CONTEXT_FILE),
2356
2452
  ("commit", "--quiet", "-m", f"sync user-context {_digest(data)}"),
2357
- ("push", "--quiet", "-u", "origin", "HEAD"),
2453
+ ("push", "--quiet", "-u", "origin", f"HEAD:refs/heads/{branch}"),
2358
2454
  )
2359
2455
  for step in steps:
2456
+ if step[0] == "push" and (
2457
+ not _context_visibility(repo) or _context_destination(repo) != branch
2458
+ ):
2459
+ return [
2460
+ *out,
2461
+ "user-context sync: SKIPPED (private destination changed before push)",
2462
+ ]
2360
2463
  done = _git(*step, cwd=CONTEXT_CLONE)
2361
2464
  if done.returncode != 0:
2362
2465
  return [
2363
2466
  *out,
2364
- f"user-context sync: FAILED at git {step[0]}: {_tail(done.stderr, 2)}",
2467
+ f"user-context sync: FAILED at git {step[0]}",
2365
2468
  ]
2366
2469
  return [*out, f"user-context sync: published ({_digest(data)})"]
2367
2470
 
@@ -1,2 +1,2 @@
1
1
  # src/roadmodel/__init__.py
2
- __version__ = "0.2.82"
2
+ __version__ = "0.2.83"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes