sp2react 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 (79) hide show
  1. sp2react/__init__.py +18 -0
  2. sp2react/agents/__init__.py +0 -0
  3. sp2react/agents/core.py +146 -0
  4. sp2react/agents/prompts/dbmap.md +28 -0
  5. sp2react/agents/prompts/fix.md +11 -0
  6. sp2react/agents/prompts/handler.md +22 -0
  7. sp2react/agents/prompts/page.md +40 -0
  8. sp2react/agents/prompts/review.md +21 -0
  9. sp2react/agents/prompts/spec.md +79 -0
  10. sp2react/agents/tasks.py +374 -0
  11. sp2react/analysis/__init__.py +0 -0
  12. sp2react/analysis/aspx.py +254 -0
  13. sp2react/analysis/behaviors.py +357 -0
  14. sp2react/analysis/fields.py +152 -0
  15. sp2react/analysis/inventory.py +314 -0
  16. sp2react/analysis/jsscan.py +330 -0
  17. sp2react/checks.py +105 -0
  18. sp2react/cli.py +188 -0
  19. sp2react/config.py +172 -0
  20. sp2react/dbmap.py +245 -0
  21. sp2react/llm/__init__.py +0 -0
  22. sp2react/llm/providers.py +219 -0
  23. sp2react/migrate.py +255 -0
  24. sp2react/net.py +60 -0
  25. sp2react/pipeline.py +583 -0
  26. sp2react/scaffold/__init__.py +0 -0
  27. sp2react/scaffold/generate.py +279 -0
  28. sp2react/scaffold/rule_vectors.json +3826 -0
  29. sp2react/scaffold/templates/backend/app/__init__.py +0 -0
  30. sp2react/scaffold/templates/backend/app/api.py +293 -0
  31. sp2react/scaffold/templates/backend/app/auth.py +33 -0
  32. sp2react/scaffold/templates/backend/app/custom/__init__.py +22 -0
  33. sp2react/scaffold/templates/backend/app/main.py +47 -0
  34. sp2react/scaffold/templates/backend/app/odata.py +185 -0
  35. sp2react/scaffold/templates/backend/app/rules.py +256 -0
  36. sp2react/scaffold/templates/backend/app/sp_repo.py +159 -0
  37. sp2react/scaffold/templates/backend/app/sql_repo.py +437 -0
  38. sp2react/scaffold/templates/backend/app/store.py +224 -0
  39. sp2react/scaffold/templates/backend/requirements.txt +8 -0
  40. sp2react/scaffold/templates/backend/tests/conftest.py +10 -0
  41. sp2react/scaffold/templates/backend/tests/test_rule_vectors.py +38 -0
  42. sp2react/scaffold/templates/frontend/index.html +12 -0
  43. sp2react/scaffold/templates/frontend/package.json +24 -0
  44. sp2react/scaffold/templates/frontend/scripts/check-rules.ts +23 -0
  45. sp2react/scaffold/templates/frontend/src/api.ts +88 -0
  46. sp2react/scaffold/templates/frontend/src/components/sp/Banner.tsx +4 -0
  47. sp2react/scaffold/templates/frontend/src/components/sp/DataTable.tsx +38 -0
  48. sp2react/scaffold/templates/frontend/src/components/sp/Form.tsx +252 -0
  49. sp2react/scaffold/templates/frontend/src/components/sp/GenericPage.tsx +28 -0
  50. sp2react/scaffold/templates/frontend/src/components/sp/Layout.tsx +54 -0
  51. sp2react/scaffold/templates/frontend/src/components/sp/LegacyHtml.tsx +9 -0
  52. sp2react/scaffold/templates/frontend/src/components/sp/ListView.tsx +59 -0
  53. sp2react/scaffold/templates/frontend/src/components/sp/Tiles.tsx +21 -0
  54. sp2react/scaffold/templates/frontend/src/components/sp/fields.tsx +132 -0
  55. sp2react/scaffold/templates/frontend/src/custom/types.ts +10 -0
  56. sp2react/scaffold/templates/frontend/src/main.tsx +14 -0
  57. sp2react/scaffold/templates/frontend/src/runtime/format.ts +36 -0
  58. sp2react/scaffold/templates/frontend/src/runtime/rules.ts +225 -0
  59. sp2react/scaffold/templates/frontend/src/runtime/spec.ts +102 -0
  60. sp2react/scaffold/templates/frontend/src/runtime/user.tsx +21 -0
  61. sp2react/scaffold/templates/frontend/src/screens/LegacyRedirect.tsx +27 -0
  62. sp2react/scaffold/templates/frontend/src/screens/ListFormScreen.tsx +33 -0
  63. sp2react/scaffold/templates/frontend/src/styles/sp.css +95 -0
  64. sp2react/scaffold/templates/frontend/src/vite-env.d.ts +1 -0
  65. sp2react/scaffold/templates/frontend/tsconfig.json +18 -0
  66. sp2react/scaffold/templates/frontend/vite.config.ts +7 -0
  67. sp2react/sharepoint/__init__.py +0 -0
  68. sp2react/sharepoint/client.py +210 -0
  69. sp2react/sharepoint/extract.py +217 -0
  70. sp2react/spec/__init__.py +0 -0
  71. sp2react/spec/base.py +80 -0
  72. sp2react/spec/coverage.py +191 -0
  73. sp2react/spec/model.py +133 -0
  74. sp2react-0.1.0.dist-info/METADATA +238 -0
  75. sp2react-0.1.0.dist-info/RECORD +79 -0
  76. sp2react-0.1.0.dist-info/WHEEL +5 -0
  77. sp2react-0.1.0.dist-info/entry_points.txt +2 -0
  78. sp2react-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. sp2react-0.1.0.dist-info/top_level.txt +1 -0
sp2react/checks.py ADDED
@@ -0,0 +1,105 @@
1
+ """Stage 8 (test against the original): build and test the generated app. Plain processes, no model."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass
14
+ class Check:
15
+ name: str
16
+ ok: bool
17
+ output: str
18
+ skipped: bool = False
19
+
20
+ def tail(self, n: int = 6000) -> str:
21
+ return self.output[-n:]
22
+
23
+
24
+ def _run(cmd: list[str], cwd: Path, timeout: int, env: dict | None = None) -> tuple[int, str]:
25
+ try:
26
+ r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout, env={**os.environ, **(env or {})})
27
+ return r.returncode, (r.stdout or "") + (r.stderr or "")
28
+ except subprocess.TimeoutExpired as e:
29
+ return 124, f"timed out after {timeout}s: {' '.join(cmd)}\n{(e.stdout or '')}"
30
+ except FileNotFoundError as e:
31
+ return 127, str(e)
32
+
33
+
34
+ def backend_tests(out: Path, timeout: int = 900, env: dict | None = None) -> Check:
35
+ be = out / "backend"
36
+ db = be / "data" / "test.db"
37
+ if db.exists():
38
+ db.unlink()
39
+ code, txt = _run([sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider", "tests"], be, timeout,
40
+ {"PYTHONPATH": str(be), "DB_PATH": str(db), **(env or {})})
41
+ return Check("backend tests (generated API tests + rule vectors)", code == 0, txt)
42
+
43
+
44
+ def frontend_install(out: Path, timeout: int = 1200) -> Check:
45
+ fe = out / "frontend"
46
+ if (fe / "node_modules" / ".bin" / "tsc").exists():
47
+ return Check("npm install", True, "node_modules present", skipped=True)
48
+ npm = shutil.which("npm")
49
+ if not npm:
50
+ return Check("npm install", False, "npm not found on PATH; install Node.js 20+ or set pipeline.run_builds: false", skipped=True)
51
+ code, txt = _run([npm, "install", "--no-audit", "--no-fund", "--loglevel=error"], fe, timeout)
52
+ return Check("npm install", code == 0, txt)
53
+
54
+
55
+ def frontend_build(out: Path, timeout: int = 900) -> Check:
56
+ fe = out / "frontend"
57
+ npm = shutil.which("npm")
58
+ if not npm or not (fe / "node_modules").exists():
59
+ return Check("frontend build (tsc + vite)", False, "dependencies not installed", skipped=True)
60
+ code, txt = _run([npm, "run", "build", "--silent"], fe, timeout)
61
+ return Check("frontend build (tsc + vite)", code == 0, txt)
62
+
63
+
64
+ def frontend_rule_vectors(out: Path, timeout: int = 120) -> Check:
65
+ node = shutil.which("node")
66
+ fe = out / "frontend"
67
+ if not node:
68
+ return Check("rule vectors (TypeScript engine)", False, "node not found", skipped=True)
69
+ code, txt = _run([node, "scripts/check-rules.ts"], fe, timeout)
70
+ if code != 0 and ("Unknown file extension" in txt or "ERR_UNKNOWN_FILE_EXTENSION" in txt):
71
+ code, txt = _run([node, "--experimental-strip-types", "scripts/check-rules.ts"], fe, timeout)
72
+ if code != 0 and "bad option" in txt:
73
+ return Check("rule vectors (TypeScript engine)", False, "Node.js >= 22.6 is needed to run the TypeScript vectors directly", skipped=True)
74
+ return Check("rule vectors (TypeScript engine)", code == 0, txt)
75
+
76
+
77
+ def run_all(out: Path, run_builds: bool = True, env: dict | None = None) -> list[Check]:
78
+ checks = [backend_tests(out, env=env)]
79
+ if run_builds:
80
+ inst = frontend_install(out)
81
+ checks.append(inst)
82
+ if inst.ok:
83
+ checks.append(frontend_build(out))
84
+ checks.append(frontend_rule_vectors(out))
85
+ return checks
86
+
87
+
88
+ def failures_by_screen(checks: list[Check], screen_ids: list[str], handlers: dict[str, str]) -> dict[str, str]:
89
+ """Route failure output to the screen whose files are named in it; '' key = infrastructure."""
90
+ out: dict[str, list[str]] = {}
91
+ for c in checks:
92
+ if c.ok or c.skipped:
93
+ continue
94
+ hit = False
95
+ for sid in screen_ids:
96
+ if re.search(rf"screens/{re.escape(sid)}/", c.output) or re.search(rf"test_{re.escape(sid.lower().replace('-', '_'))}", c.output):
97
+ out.setdefault(sid, []).append(f"## {c.name}\n{c.tail()}")
98
+ hit = True
99
+ for handler, sid in handlers.items():
100
+ if re.search(rf"custom/{re.escape(handler)}\.", c.output):
101
+ out.setdefault(sid, []).append(f"## {c.name}\n{c.tail()}")
102
+ hit = True
103
+ if not hit:
104
+ out.setdefault("", []).append(f"## {c.name}\n{c.tail()}")
105
+ return {k: "\n\n".join(v) for k, v in out.items()}
sp2react/cli.py ADDED
@@ -0,0 +1,188 @@
1
+ """sp2react command line."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .config import TEMPLATE, ConfigError, load_config
12
+
13
+ DEFAULT_CONFIG = "sp2react.yaml"
14
+
15
+
16
+ def _pipeline(args):
17
+ from .pipeline import Pipeline
18
+ cfg = load_config(args.config)
19
+ if getattr(args, "auto_approve", False):
20
+ cfg.pipeline.auto_approve = True
21
+ if getattr(args, "no_builds", False):
22
+ cfg.pipeline.run_builds = False
23
+ return Pipeline(cfg, progress=lambda m: print(m, flush=True))
24
+
25
+
26
+ def cmd_init(args) -> int:
27
+ p = Path(args.config)
28
+ if p.exists() and not args.force:
29
+ print(f"{p} already exists (use --force to overwrite)")
30
+ return 1
31
+ p.write_text(TEMPLATE, encoding="utf-8")
32
+ print(f"Wrote {p}. Fill in sharepoint.site_url and the llm section, set the secret environment variables, then run: sp2react doctor")
33
+ return 0
34
+
35
+
36
+ def cmd_doctor(args) -> int:
37
+ from .llm.providers import make_llm
38
+ from .sharepoint.client import SharePointClient
39
+ cfg = load_config(args.config)
40
+ ok = True
41
+ try:
42
+ web = SharePointClient(cfg.sharepoint).web()
43
+ print(f"SharePoint: OK - '{web.get('Title')}' ({web.get('ServerRelativeUrl')})")
44
+ except Exception as e:
45
+ ok = False
46
+ print(f"SharePoint: FAILED - {e}")
47
+ try:
48
+ llm = make_llm(cfg.llm)
49
+ try:
50
+ models = llm.models()
51
+ claude = [m for m in models if "claude" in (m or "").lower()]
52
+ print(f"Model provider ({cfg.llm.provider}): OK - {len(models)} model(s){'; Claude: ' + ', '.join(claude) if claude else ''}")
53
+ if cfg.llm.model and models and cfg.llm.model not in models:
54
+ print(f" warning: configured model '{cfg.llm.model}' is not in the provider's list")
55
+ except Exception as e:
56
+ print(f"Model provider ({cfg.llm.provider}): could not list models ({e}); trying a one-line completion")
57
+ c = llm.complete("Reply with the single word: ready", [{"role": "user", "content": "ping"}], max_tokens=10)
58
+ print(f"Model call: OK - {cfg.llm.model or cfg.llm.deployment} replied '{c.text.strip()[:40]}'")
59
+ except Exception as e:
60
+ ok = False
61
+ print(f"Model provider: FAILED - {e}")
62
+ return 0 if ok else 2
63
+
64
+
65
+ def cmd_run(args) -> int:
66
+ p = _pipeline(args)
67
+ p.run(pages=args.page or None, offline=args.offline, until=args.until)
68
+ print(f"\nReport: {p.ws / 'report.md'}\nApp: {p.out}")
69
+ return 0
70
+
71
+
72
+ def cmd_stage(args) -> int:
73
+ from .pipeline import write_report
74
+ p = _pipeline(args)
75
+ scrs = p.screens(args.page or None)
76
+ sids = [s["id"] for s in scrs] if not args.screen else args.screen
77
+ if args.stage == "extract":
78
+ p.stage_inputs(False)
79
+ p.stage_extract(args.page or None, False)
80
+ elif args.stage == "inventory":
81
+ p.stage_inventory(args.page or None)
82
+ elif args.stage == "spec":
83
+ p.stage_spec(sids)
84
+ elif args.stage == "convert":
85
+ p.stage_convert(sids)
86
+ elif args.stage == "review":
87
+ p.stage_review(sids)
88
+ elif args.stage == "test":
89
+ p.stage_test_and_fix(sids)
90
+ elif args.stage == "migrate":
91
+ specs = [p.load_spec(s) for s in p.approved_sids()]
92
+ p._offline = False
93
+ from .scaffold.generate import generate
94
+ generate(p.out, specs, p.legacy, p.stage_migrate(specs))
95
+ elif args.stage == "map-db":
96
+ specs = [p.load_spec(s) for s in p.approved_sids()] or [p.load_spec(sc["id"]) for sc in p.screens() if (p.ws / "specs" / f"{sc['id']}.json").exists()]
97
+ ok = p.stage_map_db(specs)
98
+ print(f"{'Mapping ready' if ok else 'Mapping needs attention'}: {p.cfg.database.mapping} and {p.ws / 'db-mapping.md'}")
99
+ elif args.stage == "reconcile":
100
+ rep = p.stage_reconcile([p.load_spec(s) for s in p.approved_sids()])
101
+ print(f"{'All lists match' if rep['ok'] else 'Mismatches found'} - see {p.ws / 'reconciliation.md'}")
102
+ write_report(p)
103
+ return 0
104
+
105
+
106
+ def cmd_approve(args) -> int:
107
+ from .pipeline import write_report
108
+ p = _pipeline(args)
109
+ gate = "screen" if args.screen_gate else "spec"
110
+ if gate == "spec" and p.status(args.screen_id, "spec") != "done":
111
+ print(f"The spec for {args.screen_id} is not done yet")
112
+ return 1
113
+ if gate == "screen" and p.status(args.screen_id, "test") != "done":
114
+ print(f"{args.screen_id} has not passed testing yet")
115
+ return 1
116
+ p.approve(args.screen_id, gate, args.by or "")
117
+ write_report(p)
118
+ return 0
119
+
120
+
121
+ def cmd_status(args) -> int:
122
+ from .pipeline import STAGES
123
+ p = _pipeline(args)
124
+ if args.json:
125
+ print(json.dumps(p.state, indent=2))
126
+ return 0
127
+ for k in ("inputs", "extract"):
128
+ s = p.state["global"].get(k)
129
+ if s:
130
+ print(f"{k:<10} {s['status']:<9} {s.get('detail', '')}")
131
+ for sid, st in p.state["screens"].items():
132
+ print(f"\n{sid}")
133
+ for s in STAGES[2:]:
134
+ if st.get(s):
135
+ print(f" {s:<13} {st[s]['status']:<9} {st[s].get('detail', '')[:110]}")
136
+ return 0
137
+
138
+
139
+ def main(argv=None) -> int:
140
+ ap = argparse.ArgumentParser(prog="sp2react", description="Convert classic SharePoint ASPX/jQuery forms and pages to React + FastAPI with AI agents.")
141
+ ap.add_argument("--version", action="version", version=__version__)
142
+ ap.add_argument("-c", "--config", default=DEFAULT_CONFIG)
143
+ ap.add_argument("-v", "--verbose", action="store_true")
144
+ sub = ap.add_subparsers(dest="cmd", required=True)
145
+
146
+ s = sub.add_parser("init", help="write a starter sp2react.yaml")
147
+ s.add_argument("--force", action="store_true")
148
+ s.set_defaults(fn=cmd_init)
149
+
150
+ s = sub.add_parser("doctor", help="check the SharePoint connection and the model provider (lists Bridger's Claude models)")
151
+ s.set_defaults(fn=cmd_doctor)
152
+
153
+ s = sub.add_parser("run", help="run every stage up to the next approval gate")
154
+ s.add_argument("--page", action="append", help="site-relative or server-relative page URL (repeatable); default: all list forms + SitePages")
155
+ s.add_argument("--offline", action="store_true", help="reuse the extraction already in the workspace")
156
+ s.add_argument("--until", default="approve", choices=["inputs", "extract", "inventory", "spec", "approve_spec", "convert", "review", "test", "fix", "approve"])
157
+ s.add_argument("--auto-approve", action="store_true", help="skip the human gates (CI / demos)")
158
+ s.add_argument("--no-builds", action="store_true", help="skip npm install/build")
159
+ s.set_defaults(fn=cmd_run)
160
+
161
+ for name in ("extract", "inventory", "spec", "map-db", "convert", "review", "test", "migrate", "reconcile"):
162
+ s = sub.add_parser(name, help=f"run only the {name} stage")
163
+ s.add_argument("--page", action="append")
164
+ s.add_argument("--screen", action="append")
165
+ s.add_argument("--no-builds", action="store_true")
166
+ s.set_defaults(fn=cmd_stage, stage=name)
167
+
168
+ s = sub.add_parser("approve", help="approve a screen's spec (default) or the finished screen (--screen)")
169
+ s.add_argument("screen_id")
170
+ s.add_argument("--screen", dest="screen_gate", action="store_true")
171
+ s.add_argument("--by", help="name to record with the approval")
172
+ s.set_defaults(fn=cmd_approve)
173
+
174
+ s = sub.add_parser("status", help="show every screen's stage status")
175
+ s.add_argument("--json", action="store_true")
176
+ s.set_defaults(fn=cmd_status)
177
+
178
+ args = ap.parse_args(argv)
179
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
180
+ try:
181
+ return args.fn(args)
182
+ except ConfigError as e:
183
+ print(f"Configuration problem: {e}", file=sys.stderr)
184
+ return 2
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())
sp2react/config.py ADDED
@@ -0,0 +1,172 @@
1
+ """Configuration: one YAML file, secrets only through environment variables."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Literal, Optional, Union
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+
12
+ class ConfigError(RuntimeError):
13
+ pass
14
+
15
+
16
+ class SharePointConfig(BaseModel):
17
+ site_url: str = Field(..., description="Full URL of the site, e.g. https://sp.example.com/sites/SEA")
18
+ auth: Literal["ntlm", "cookie", "bearer", "none"] = "ntlm"
19
+ username_env: str = "SP_USERNAME" # DOMAIN\\user for NTLM
20
+ password_env: str = "SP_PASSWORD"
21
+ cookie_env: str = "SP_COOKIE" # e.g. "FedAuth=...; rtFa=..." copied from a signed-in browser
22
+ token_env: str = "SP_TOKEN" # bearer token (SharePoint Online / Entra)
23
+ verify_ssl: Union[bool, str] = True # True, False, or path to a corporate CA bundle
24
+ timeout: int = 60
25
+ sample_items: int = 200 # rows exported per list (lookup values, seed data)
26
+ extra_headers: dict[str, str] = Field(default_factory=dict)
27
+
28
+ @field_validator("site_url")
29
+ @classmethod
30
+ def _strip(cls, v: str) -> str:
31
+ return v.rstrip("/")
32
+
33
+
34
+ class LLMConfig(BaseModel):
35
+ provider: Literal["bridger", "openai", "azure_openai", "anthropic", "scripted"] = "bridger"
36
+ api_format: Literal["anthropic", "openai"] = "anthropic" # bridger only: which wire format Bridger serves Claude on
37
+ base_url: Optional[str] = None # bridger: http://host:port ; azure: https://<res>.openai.azure.com
38
+ model: Optional[str] = None # model id as the provider lists it (run `sp2react doctor`); azure uses `deployment`
39
+ api_key_env: Optional[str] = None # env var holding the key/token; omit if Bridger needs none
40
+ deployment: Optional[str] = None # azure_openai only
41
+ api_version: str = "2024-10-21" # azure_openai only
42
+ temperature: float = 0.0
43
+ max_tokens: int = 16000
44
+ timeout: int = 600 # seconds per model call
45
+ json_mode: bool = True # send response_format=json_object where supported
46
+ extra_headers: dict[str, str] = Field(default_factory=dict)
47
+ verify_ssl: Union[bool, str] = True
48
+
49
+
50
+ class PipelineConfig(BaseModel):
51
+ workspace: str = "./sp2react-work" # extraction, inventory, spec, agent transcripts, state
52
+ output: str = "./converted-app" # generated React + FastAPI app
53
+ max_concurrency: int = 4 # parallel agent calls
54
+ agent_retries: int = 2 # re-asks per agent task when its output fails validation
55
+ agent_timeout: int = 900 # hard limit per agent task (seconds)
56
+ max_fix_iterations: int = 5
57
+ auto_approve: bool = False # skip the human gates (spec + screen approval)
58
+ run_builds: bool = True # npm install/build + pytest on the generated app
59
+ data_source: Literal["database", "sqlite", "sharepoint"] = "database"
60
+ # database = your existing database (MySQL / SQL Server / PostgreSQL) through a list->table mapping;
61
+ # no list items are ever read from SharePoint
62
+ # sqlite = sp2react migrates the list items into a SQLite copy (needs sharepoint item reads)
63
+ # sharepoint = the generated API reads and writes the live lists
64
+
65
+
66
+ class DatabaseConfig(BaseModel):
67
+ url_env: str = "SP2REACT_DB_URL" # env var with a SQLAlchemy URL, e.g. mysql+pymysql://user:pass@host:3306/sea
68
+ schema_name: Optional[str] = None # database schema, if not the default
69
+ mapping: str = "./db-mapping.json" # list -> table / field -> column mapping (sp2react map-db drafts it)
70
+
71
+
72
+ class SecurityConfig(BaseModel):
73
+ allowed_hosts: list[str] = Field(default_factory=list) # extra hosts beyond the SharePoint site and model endpoint
74
+ sharepoint_item_reads: Optional[bool] = None # None: allowed only when data_source is sqlite
75
+
76
+
77
+ class Config(BaseModel):
78
+ sharepoint: SharePointConfig
79
+ llm: LLMConfig = Field(default_factory=LLMConfig)
80
+ pipeline: PipelineConfig = Field(default_factory=PipelineConfig)
81
+ database: DatabaseConfig = Field(default_factory=DatabaseConfig)
82
+ security: SecurityConfig = Field(default_factory=SecurityConfig)
83
+
84
+ @property
85
+ def item_reads_allowed(self) -> bool:
86
+ """Whether sp2react may read list ITEMS (business data) from SharePoint over HTTP."""
87
+ if self.security.sharepoint_item_reads is not None:
88
+ return self.security.sharepoint_item_reads
89
+ return self.pipeline.data_source == "sqlite"
90
+
91
+ @property
92
+ def workspace(self) -> Path:
93
+ return Path(self.pipeline.workspace).resolve()
94
+
95
+ @property
96
+ def output(self) -> Path:
97
+ return Path(self.pipeline.output).resolve()
98
+
99
+
100
+ def secret(env_name: Optional[str], what: str, required: bool = True) -> Optional[str]:
101
+ if not env_name:
102
+ if required:
103
+ raise ConfigError(f"No environment variable configured for {what}.")
104
+ return None
105
+ value = os.environ.get(env_name)
106
+ if not value and required:
107
+ raise ConfigError(f"Set the environment variable {env_name} ({what}).")
108
+ return value
109
+
110
+
111
+ def load_config(path: Union[str, Path]) -> Config:
112
+ p = Path(path)
113
+ if not p.exists():
114
+ raise ConfigError(f"Config file not found: {p}. Run `sp2react init` to create one.")
115
+ data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
116
+ try:
117
+ cfg = Config.model_validate(data)
118
+ except Exception as e: # pydantic ValidationError -> readable message
119
+ raise ConfigError(f"Invalid config {p}:\n{e}") from e
120
+ # Relative workspace/output paths are relative to the config file, not the cwd
121
+ base = p.resolve().parent
122
+ for attr in ("workspace", "output"):
123
+ val = getattr(cfg.pipeline, attr)
124
+ if not Path(val).is_absolute():
125
+ setattr(cfg.pipeline, attr, str((base / val).resolve()))
126
+ if not Path(cfg.database.mapping).is_absolute():
127
+ cfg.database.mapping = str((base / cfg.database.mapping).resolve())
128
+ return cfg
129
+
130
+
131
+ TEMPLATE = """\
132
+ # sp2react configuration. Secrets are read from environment variables, never stored here.
133
+ sharepoint:
134
+ site_url: https://sharepoint.example.com/sites/SEA
135
+ auth: ntlm # ntlm | cookie | bearer | none
136
+ username_env: SP_USERNAME # DOMAIN\\\\user (ntlm)
137
+ password_env: SP_PASSWORD # (ntlm)
138
+ cookie_env: SP_COOKIE # "FedAuth=...; rtFa=..." from a signed-in browser (cookie)
139
+ token_env: SP_TOKEN # bearer token (bearer)
140
+ verify_ssl: true # or a path to your corporate CA bundle
141
+ sample_items: 200
142
+
143
+ llm:
144
+ provider: bridger # bridger | anthropic | openai | azure_openai
145
+ api_format: anthropic # how Bridger serves Claude: anthropic (/v1/messages) or openai (/v1/chat/completions)
146
+ base_url: http://localhost:4141
147
+ model: claude-sonnet-4 # exact Claude model id as Bridger lists it: run `sp2react doctor`
148
+ api_key_env: BRIDGER_TOKEN # omit if Bridger needs no token
149
+ # azure_openai: base_url: https://<resource>.openai.azure.com, deployment: <name>, api_key_env: AZURE_OPENAI_API_KEY
150
+ # anthropic: model: claude-sonnet-5-5, api_key_env: ANTHROPIC_API_KEY
151
+ # openai: base_url: https://api.openai.com/v1, model: <model>, api_key_env: OPENAI_API_KEY
152
+
153
+ pipeline:
154
+ workspace: ./sp2react-work
155
+ output: ./converted-app
156
+ max_concurrency: 4
157
+ agent_retries: 2
158
+ agent_timeout: 900
159
+ max_fix_iterations: 5
160
+ auto_approve: false
161
+ run_builds: true
162
+ data_source: database # database: your existing MySQL/SQL Server tables (no list items read over HTTP)
163
+ # sqlite: sp2react migrates the items itself | sharepoint: API uses the live lists
164
+
165
+ database:
166
+ url_env: SP2REACT_DB_URL # e.g. mysql+pymysql://user:password@db-host:3306/sea (read from the environment)
167
+ mapping: ./db-mapping.json # drafted by `sp2react map-db`, reviewed by you
168
+
169
+ security:
170
+ allowed_hosts: [] # every request is refused unless it goes to the SharePoint site or the model endpoint
171
+ # sharepoint_item_reads: false # default: false unless data_source is sqlite
172
+ """