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.
- sp2react/__init__.py +18 -0
- sp2react/agents/__init__.py +0 -0
- sp2react/agents/core.py +146 -0
- sp2react/agents/prompts/dbmap.md +28 -0
- sp2react/agents/prompts/fix.md +11 -0
- sp2react/agents/prompts/handler.md +22 -0
- sp2react/agents/prompts/page.md +40 -0
- sp2react/agents/prompts/review.md +21 -0
- sp2react/agents/prompts/spec.md +79 -0
- sp2react/agents/tasks.py +374 -0
- sp2react/analysis/__init__.py +0 -0
- sp2react/analysis/aspx.py +254 -0
- sp2react/analysis/behaviors.py +357 -0
- sp2react/analysis/fields.py +152 -0
- sp2react/analysis/inventory.py +314 -0
- sp2react/analysis/jsscan.py +330 -0
- sp2react/checks.py +105 -0
- sp2react/cli.py +188 -0
- sp2react/config.py +172 -0
- sp2react/dbmap.py +245 -0
- sp2react/llm/__init__.py +0 -0
- sp2react/llm/providers.py +219 -0
- sp2react/migrate.py +255 -0
- sp2react/net.py +60 -0
- sp2react/pipeline.py +583 -0
- sp2react/scaffold/__init__.py +0 -0
- sp2react/scaffold/generate.py +279 -0
- sp2react/scaffold/rule_vectors.json +3826 -0
- sp2react/scaffold/templates/backend/app/__init__.py +0 -0
- sp2react/scaffold/templates/backend/app/api.py +293 -0
- sp2react/scaffold/templates/backend/app/auth.py +33 -0
- sp2react/scaffold/templates/backend/app/custom/__init__.py +22 -0
- sp2react/scaffold/templates/backend/app/main.py +47 -0
- sp2react/scaffold/templates/backend/app/odata.py +185 -0
- sp2react/scaffold/templates/backend/app/rules.py +256 -0
- sp2react/scaffold/templates/backend/app/sp_repo.py +159 -0
- sp2react/scaffold/templates/backend/app/sql_repo.py +437 -0
- sp2react/scaffold/templates/backend/app/store.py +224 -0
- sp2react/scaffold/templates/backend/requirements.txt +8 -0
- sp2react/scaffold/templates/backend/tests/conftest.py +10 -0
- sp2react/scaffold/templates/backend/tests/test_rule_vectors.py +38 -0
- sp2react/scaffold/templates/frontend/index.html +12 -0
- sp2react/scaffold/templates/frontend/package.json +24 -0
- sp2react/scaffold/templates/frontend/scripts/check-rules.ts +23 -0
- sp2react/scaffold/templates/frontend/src/api.ts +88 -0
- sp2react/scaffold/templates/frontend/src/components/sp/Banner.tsx +4 -0
- sp2react/scaffold/templates/frontend/src/components/sp/DataTable.tsx +38 -0
- sp2react/scaffold/templates/frontend/src/components/sp/Form.tsx +252 -0
- sp2react/scaffold/templates/frontend/src/components/sp/GenericPage.tsx +28 -0
- sp2react/scaffold/templates/frontend/src/components/sp/Layout.tsx +54 -0
- sp2react/scaffold/templates/frontend/src/components/sp/LegacyHtml.tsx +9 -0
- sp2react/scaffold/templates/frontend/src/components/sp/ListView.tsx +59 -0
- sp2react/scaffold/templates/frontend/src/components/sp/Tiles.tsx +21 -0
- sp2react/scaffold/templates/frontend/src/components/sp/fields.tsx +132 -0
- sp2react/scaffold/templates/frontend/src/custom/types.ts +10 -0
- sp2react/scaffold/templates/frontend/src/main.tsx +14 -0
- sp2react/scaffold/templates/frontend/src/runtime/format.ts +36 -0
- sp2react/scaffold/templates/frontend/src/runtime/rules.ts +225 -0
- sp2react/scaffold/templates/frontend/src/runtime/spec.ts +102 -0
- sp2react/scaffold/templates/frontend/src/runtime/user.tsx +21 -0
- sp2react/scaffold/templates/frontend/src/screens/LegacyRedirect.tsx +27 -0
- sp2react/scaffold/templates/frontend/src/screens/ListFormScreen.tsx +33 -0
- sp2react/scaffold/templates/frontend/src/styles/sp.css +95 -0
- sp2react/scaffold/templates/frontend/src/vite-env.d.ts +1 -0
- sp2react/scaffold/templates/frontend/tsconfig.json +18 -0
- sp2react/scaffold/templates/frontend/vite.config.ts +7 -0
- sp2react/sharepoint/__init__.py +0 -0
- sp2react/sharepoint/client.py +210 -0
- sp2react/sharepoint/extract.py +217 -0
- sp2react/spec/__init__.py +0 -0
- sp2react/spec/base.py +80 -0
- sp2react/spec/coverage.py +191 -0
- sp2react/spec/model.py +133 -0
- sp2react-0.1.0.dist-info/METADATA +238 -0
- sp2react-0.1.0.dist-info/RECORD +79 -0
- sp2react-0.1.0.dist-info/WHEEL +5 -0
- sp2react-0.1.0.dist-info/entry_points.txt +2 -0
- sp2react-0.1.0.dist-info/licenses/LICENSE +21 -0
- sp2react-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Stage 4 gate: the spec may only be approved when it accounts for every 'must' behaviour.
|
|
2
|
+
|
|
3
|
+
Returns plain-language problems; the spec agent gets them back verbatim on retry.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from .model import NEEDS_FIELD, NEEDS_MESSAGE, RULE_PARAMS, Spec
|
|
12
|
+
|
|
13
|
+
OPS = {"eq", "ne", "in", "nin", "gt", "gte", "lt", "lte", "empty", "notEmpty", "changed"}
|
|
14
|
+
COMPARE_OPS = {"eq", "ne", "gt", "gte", "lt", "lte"}
|
|
15
|
+
ENDPOINT_PATH = re.compile(r"^/api/[A-Za-z0-9_\-/{}.]+$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CoverageReport:
|
|
20
|
+
ok: bool
|
|
21
|
+
problems: list[str] = field(default_factory=list)
|
|
22
|
+
covered: int = 0
|
|
23
|
+
must: int = 0
|
|
24
|
+
not_carried: int = 0
|
|
25
|
+
matrix: list[dict] = field(default_factory=list) # behavior -> what covers it
|
|
26
|
+
|
|
27
|
+
def summary(self) -> str:
|
|
28
|
+
return f"{self.covered}/{self.must} required behaviours covered ({self.not_carried} not carried, with reasons); {len(self.problems)} problem(s)"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _norm(msg: str) -> str:
|
|
32
|
+
m = re.sub(r"\{[^{}]*\}", "{}", msg or "")
|
|
33
|
+
return re.sub(r"\s+", " ", m).strip()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _condition_problems(cond: Any, fields: set[str], groups: set[str], where: str) -> list[str]:
|
|
37
|
+
out = []
|
|
38
|
+
if not isinstance(cond, dict) or not cond:
|
|
39
|
+
return [f"{where}: condition must be a non-empty object"]
|
|
40
|
+
keys = set(cond)
|
|
41
|
+
if keys & {"all", "any"}:
|
|
42
|
+
k = "all" if "all" in cond else "any"
|
|
43
|
+
if not isinstance(cond[k], list) or not cond[k]:
|
|
44
|
+
return [f"{where}: '{k}' needs a non-empty list"]
|
|
45
|
+
for i, c in enumerate(cond[k]):
|
|
46
|
+
out += _condition_problems(c, fields, groups, f"{where}.{k}[{i}]")
|
|
47
|
+
return out
|
|
48
|
+
if "not" in keys:
|
|
49
|
+
return _condition_problems(cond["not"], fields, groups, f"{where}.not")
|
|
50
|
+
if "role" in keys:
|
|
51
|
+
if cond["role"] not in groups:
|
|
52
|
+
out.append(f"{where}: role '{cond['role']}' is not one of the spec roles {sorted(groups)}")
|
|
53
|
+
return out
|
|
54
|
+
if "mode" in keys:
|
|
55
|
+
ms = cond["mode"] if isinstance(cond["mode"], list) else [cond["mode"]]
|
|
56
|
+
bad = [m for m in ms if m not in ("new", "edit", "display")]
|
|
57
|
+
return [f"{where}: unknown mode {bad}"] if bad else []
|
|
58
|
+
if "field" in keys:
|
|
59
|
+
f = str(cond["field"]).split(".")[0]
|
|
60
|
+
if f not in fields and f not in ("ID", "Author", "Editor", "Created", "Modified"):
|
|
61
|
+
out.append(f"{where}: field '{cond['field']}' does not exist")
|
|
62
|
+
ops = keys - {"field"}
|
|
63
|
+
if not ops or not ops <= OPS:
|
|
64
|
+
out.append(f"{where}: needs one operator from {sorted(OPS)}, got {sorted(ops)}")
|
|
65
|
+
return out
|
|
66
|
+
return [f"{where}: unknown condition shape {sorted(keys)} (use field/role/mode/all/any/not)"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check(spec: Spec, inv: dict) -> CoverageReport:
|
|
70
|
+
problems: list[str] = []
|
|
71
|
+
behaviors = {b["id"]: b for b in inv["behaviors"]}
|
|
72
|
+
must = {k: b for k, b in behaviors.items() if b["level"] == "must"}
|
|
73
|
+
covers = spec.all_covers()
|
|
74
|
+
|
|
75
|
+
# 1. no invented ids
|
|
76
|
+
for cid, by in covers.items():
|
|
77
|
+
if cid not in behaviors:
|
|
78
|
+
problems.append(f"{by[0]} covers '{cid}', which is not in the inventory. Use ids exactly as listed.")
|
|
79
|
+
|
|
80
|
+
# 2. every must behaviour is accounted for
|
|
81
|
+
matrix = []
|
|
82
|
+
for bid, b in must.items():
|
|
83
|
+
by = covers.get(bid, [])
|
|
84
|
+
matrix.append({"behavior": bid, "kind": b["kind"], "covered_by": by})
|
|
85
|
+
if not by:
|
|
86
|
+
hint = f" message: \"{b['message']}\"" if b.get("message") else ""
|
|
87
|
+
problems.append(f"Not covered: {bid} ({b['kind']}, `{b.get('code', '')[:90]}`){hint}")
|
|
88
|
+
|
|
89
|
+
# 3. messages are carried verbatim by whatever covers the behaviour
|
|
90
|
+
texts: list[str] = []
|
|
91
|
+
|
|
92
|
+
def collect(v) -> None:
|
|
93
|
+
if isinstance(v, str):
|
|
94
|
+
texts.append(_norm(v))
|
|
95
|
+
elif isinstance(v, dict):
|
|
96
|
+
for x in v.values():
|
|
97
|
+
collect(x)
|
|
98
|
+
elif isinstance(v, list):
|
|
99
|
+
for x in v:
|
|
100
|
+
collect(x)
|
|
101
|
+
|
|
102
|
+
for r in spec.rules:
|
|
103
|
+
if r.message:
|
|
104
|
+
texts.append(_norm(r.message))
|
|
105
|
+
collect(r.params)
|
|
106
|
+
for c in spec.screen.components:
|
|
107
|
+
collect(c.props)
|
|
108
|
+
not_carried_ids = {c for n in spec.not_carried for c in n.covers}
|
|
109
|
+
for bid, b in must.items():
|
|
110
|
+
if bid in not_carried_ids:
|
|
111
|
+
continue
|
|
112
|
+
msgs = [b["message"]] if b.get("message") else list((b.get("detail") or {}).get("messages", []))
|
|
113
|
+
for m in msgs:
|
|
114
|
+
if _norm(m) and _norm(m) not in texts:
|
|
115
|
+
problems.append(f"Message from {bid} is not carried verbatim. Use exactly: \"{m}\" (placeholders like {{name}} may differ)")
|
|
116
|
+
|
|
117
|
+
# 4. rules are well formed
|
|
118
|
+
fields_by_list = {k: {f["name"] for f in v.fields} for k, v in spec.lists.items()}
|
|
119
|
+
groups = set(spec.roles)
|
|
120
|
+
ids = set()
|
|
121
|
+
for r in spec.rules:
|
|
122
|
+
where = f"rule {r.id}"
|
|
123
|
+
if r.id in ids:
|
|
124
|
+
problems.append(f"{where}: duplicate rule id")
|
|
125
|
+
ids.add(r.id)
|
|
126
|
+
lst = r.list or spec.screen.list
|
|
127
|
+
flds = fields_by_list.get(lst or "", set())
|
|
128
|
+
if r.kind in NEEDS_FIELD and not r.field:
|
|
129
|
+
problems.append(f"{where}: kind '{r.kind}' needs 'field'")
|
|
130
|
+
if r.field and r.field not in flds:
|
|
131
|
+
problems.append(f"{where}: field '{r.field}' is not in list {lst}")
|
|
132
|
+
for p in RULE_PARAMS.get(r.kind, ()):
|
|
133
|
+
if p not in r.params:
|
|
134
|
+
problems.append(f"{where}: kind '{r.kind}' needs params.{p}")
|
|
135
|
+
if r.kind in NEEDS_MESSAGE and not r.message:
|
|
136
|
+
problems.append(f"{where}: kind '{r.kind}' needs a message")
|
|
137
|
+
if r.kind in ("compare",) and r.params.get("other") and r.params["other"] not in flds:
|
|
138
|
+
problems.append(f"{where}: params.other '{r.params['other']}' is not in list {lst}")
|
|
139
|
+
if r.kind in ("compare", "length", "range") and r.params.get("op") not in COMPARE_OPS:
|
|
140
|
+
problems.append(f"{where}: params.op must be one of {sorted(COMPARE_OPS)}")
|
|
141
|
+
if r.kind == "derive":
|
|
142
|
+
src = r.params.get("from_field")
|
|
143
|
+
if src not in flds:
|
|
144
|
+
problems.append(f"{where}: params.from_field '{src}' is not in list {lst}")
|
|
145
|
+
else:
|
|
146
|
+
f = next(x for x in spec.lists[lst].fields if x["name"] == src)
|
|
147
|
+
target = (f.get("lookup") or {}).get("list")
|
|
148
|
+
if not target:
|
|
149
|
+
problems.append(f"{where}: params.from_field '{src}' must be a lookup field")
|
|
150
|
+
elif target in spec.lists and r.params.get("column") not in fields_by_list[target]:
|
|
151
|
+
problems.append(f"{where}: params.column '{r.params.get('column')}' is not a field of {target}")
|
|
152
|
+
if r.kind == "cascade":
|
|
153
|
+
for p in ("parent",):
|
|
154
|
+
if r.params.get(p) not in flds:
|
|
155
|
+
problems.append(f"{where}: params.{p} '{r.params.get(p)}' is not in list {lst}")
|
|
156
|
+
if r.kind == "on_save":
|
|
157
|
+
act = r.params.get("action")
|
|
158
|
+
if not isinstance(act, dict) or act.get("type") not in ("create_item", "update_item", "custom"):
|
|
159
|
+
problems.append(f"{where}: params.action must be an object with type create_item | update_item | custom")
|
|
160
|
+
elif act.get("type") == "create_item" and act.get("list") not in spec.lists:
|
|
161
|
+
problems.append(f"{where}: params.action.list '{act.get('list')}' is not one of the spec lists")
|
|
162
|
+
if r.when is not None:
|
|
163
|
+
problems += _condition_problems(r.when, flds, groups, f"{where}.when")
|
|
164
|
+
if r.covers and not r.src and r.origin == "agent":
|
|
165
|
+
problems.append(f"{where}: covers behaviours but has no src (file:line)")
|
|
166
|
+
|
|
167
|
+
# 5. endpoints and components
|
|
168
|
+
eids = set()
|
|
169
|
+
for e in spec.endpoints:
|
|
170
|
+
if e.id in eids:
|
|
171
|
+
problems.append(f"endpoint {e.id}: duplicate id")
|
|
172
|
+
eids.add(e.id)
|
|
173
|
+
if not ENDPOINT_PATH.match(e.path):
|
|
174
|
+
problems.append(f"endpoint {e.id}: path '{e.path}' must start with /api/ and contain only path characters")
|
|
175
|
+
for g in e.roles:
|
|
176
|
+
if g not in groups:
|
|
177
|
+
problems.append(f"endpoint {e.id}: role '{g}' is not one of the spec roles")
|
|
178
|
+
for c in spec.screen.components:
|
|
179
|
+
for d in c.data:
|
|
180
|
+
if d not in eids:
|
|
181
|
+
problems.append(f"component {c.id}: data '{d}' is not an endpoint id")
|
|
182
|
+
for g in c.roles:
|
|
183
|
+
if g not in groups:
|
|
184
|
+
problems.append(f"component {c.id}: role '{g}' is not one of the spec roles")
|
|
185
|
+
for n in spec.not_carried:
|
|
186
|
+
if len(n.reason.strip()) < 15:
|
|
187
|
+
problems.append(f"not_carried {n.covers}: give a real reason (why the new app does not need it)")
|
|
188
|
+
|
|
189
|
+
covered = sum(1 for m in matrix if m["covered_by"] and m["covered_by"] != ["not_carried"])
|
|
190
|
+
nc = sum(1 for m in matrix if m["covered_by"] == ["not_carried"])
|
|
191
|
+
return CoverageReport(ok=not problems, problems=problems, covered=covered + nc, must=len(must), not_carried=nc, matrix=matrix)
|
sp2react/spec/model.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""The screen spec: the single contract between analysis, agents, code generation and the runtime.
|
|
2
|
+
|
|
3
|
+
The generated app interprets rules at runtime (same engine in TypeScript and Python), so a rule
|
|
4
|
+
written once is enforced in the browser and again on the server.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
Mode = Literal["new", "edit", "display"]
|
|
14
|
+
|
|
15
|
+
RULE_KINDS = (
|
|
16
|
+
"visible", "required", "readonly", "default", "derive", "cascade", "compare", "length", "range", "pattern",
|
|
17
|
+
"unique", "set_on_save", "lock_item", "on_save", "banner", "confirm", "navigate", "custom",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# params each rule kind must carry
|
|
21
|
+
RULE_PARAMS: Dict[str, tuple[str, ...]] = {
|
|
22
|
+
"default": ("value",),
|
|
23
|
+
"derive": ("from_field", "column"),
|
|
24
|
+
"cascade": ("parent", "via"),
|
|
25
|
+
"compare": ("op", "other"),
|
|
26
|
+
"length": ("op", "value"),
|
|
27
|
+
"range": ("op", "value"),
|
|
28
|
+
"pattern": ("regex",),
|
|
29
|
+
"set_on_save": ("value",),
|
|
30
|
+
"on_save": ("action",),
|
|
31
|
+
"navigate": ("to",),
|
|
32
|
+
"custom": ("handler", "description"),
|
|
33
|
+
}
|
|
34
|
+
NEEDS_FIELD = {"visible", "required", "readonly", "default", "derive", "cascade", "compare", "length", "range", "pattern", "unique", "set_on_save"}
|
|
35
|
+
NEEDS_MESSAGE = {"compare", "length", "range", "pattern", "unique", "lock_item", "banner", "confirm"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Rule(BaseModel):
|
|
39
|
+
id: str
|
|
40
|
+
kind: Literal[RULE_KINDS] # type: ignore[valid-type]
|
|
41
|
+
field: Optional[str] = None
|
|
42
|
+
list: Optional[str] = None # defaults to the screen's primary list
|
|
43
|
+
modes: List[Mode] = Field(default_factory=builtins.list) # empty = every mode
|
|
44
|
+
when: Optional[Dict[str, Any]] = None
|
|
45
|
+
message: Optional[str] = None # verbatim legacy text; {field} placeholders allowed
|
|
46
|
+
presentation: Literal["inline", "alert", "banner", "confirm"] = "inline"
|
|
47
|
+
params: Dict[str, Any] = Field(default_factory=builtins.dict)
|
|
48
|
+
covers: List[str] = Field(default_factory=builtins.list)
|
|
49
|
+
src: List[str] = Field(default_factory=builtins.list) # "file:line" references into the legacy source
|
|
50
|
+
origin: Literal["schema", "inventory", "agent"] = "agent"
|
|
51
|
+
note: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Endpoint(BaseModel):
|
|
55
|
+
id: str
|
|
56
|
+
method: Literal["GET", "POST", "PATCH", "DELETE"]
|
|
57
|
+
path: str
|
|
58
|
+
purpose: str
|
|
59
|
+
list: Optional[str] = None
|
|
60
|
+
query: Dict[str, Any] = Field(default_factory=builtins.dict) # filter/select/expand/orderby/top in the API's OData subset
|
|
61
|
+
body: List[str] = Field(default_factory=builtins.list)
|
|
62
|
+
roles: List[str] = Field(default_factory=builtins.list) # SharePoint group names allowed; empty = any signed-in user
|
|
63
|
+
custom: bool = False # needs an agent-written handler (not the generic list API)
|
|
64
|
+
covers: List[str] = Field(default_factory=builtins.list)
|
|
65
|
+
src: List[str] = Field(default_factory=builtins.list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Component(BaseModel):
|
|
69
|
+
id: str
|
|
70
|
+
kind: Literal["form", "tiles", "table", "list_view", "banner", "html", "actions", "custom"]
|
|
71
|
+
title: Optional[str] = None
|
|
72
|
+
region: Optional[str] = None # zone / column, to keep the legacy layout
|
|
73
|
+
order: int = 0
|
|
74
|
+
data: List[str] = Field(default_factory=builtins.list) # endpoint ids this component reads/writes
|
|
75
|
+
props: Dict[str, Any] = Field(default_factory=builtins.dict)
|
|
76
|
+
roles: List[str] = Field(default_factory=builtins.list) # visible only to these SharePoint groups
|
|
77
|
+
covers: List[str] = Field(default_factory=builtins.list)
|
|
78
|
+
src: List[str] = Field(default_factory=builtins.list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Screen(BaseModel):
|
|
82
|
+
id: str
|
|
83
|
+
kind: Literal["list-form", "page"]
|
|
84
|
+
title: str
|
|
85
|
+
route: str
|
|
86
|
+
legacy_urls: List[str] = Field(default_factory=builtins.list)
|
|
87
|
+
list: Optional[str] = None
|
|
88
|
+
modes: List[Mode] = Field(default_factory=builtins.list)
|
|
89
|
+
components: List[Component] = Field(default_factory=builtins.list)
|
|
90
|
+
stylesheets: List[str] = Field(default_factory=builtins.list)
|
|
91
|
+
covers: List[str] = Field(default_factory=builtins.list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ListSpec(BaseModel):
|
|
95
|
+
title: str
|
|
96
|
+
primary: bool = False
|
|
97
|
+
fields: List[Dict[str, Any]]
|
|
98
|
+
attachments: bool = False
|
|
99
|
+
views: Dict[str, Any] = Field(default_factory=builtins.dict)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class NotCarried(BaseModel):
|
|
103
|
+
covers: List[str]
|
|
104
|
+
reason: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Spec(BaseModel):
|
|
108
|
+
version: int = 1
|
|
109
|
+
screen: Screen
|
|
110
|
+
lists: Dict[str, ListSpec]
|
|
111
|
+
rules: List[Rule] = Field(default_factory=builtins.list)
|
|
112
|
+
endpoints: List[Endpoint] = Field(default_factory=builtins.list)
|
|
113
|
+
roles: List[str] = Field(default_factory=builtins.list) # SharePoint groups the screen checks
|
|
114
|
+
not_carried: List[NotCarried] = Field(default_factory=builtins.list)
|
|
115
|
+
open_questions: List[str] = Field(default_factory=builtins.list)
|
|
116
|
+
|
|
117
|
+
def all_covers(self) -> Dict[str, List[str]]:
|
|
118
|
+
out: Dict[str, List[str]] = {}
|
|
119
|
+
for r in self.rules:
|
|
120
|
+
for c in r.covers:
|
|
121
|
+
out.setdefault(c, []).append(f"rule:{r.id}")
|
|
122
|
+
for e in self.endpoints:
|
|
123
|
+
for c in e.covers:
|
|
124
|
+
out.setdefault(c, []).append(f"endpoint:{e.id}")
|
|
125
|
+
for comp in self.screen.components:
|
|
126
|
+
for c in comp.covers:
|
|
127
|
+
out.setdefault(c, []).append(f"component:{comp.id}")
|
|
128
|
+
for c in self.screen.covers:
|
|
129
|
+
out.setdefault(c, []).append("screen")
|
|
130
|
+
for n in self.not_carried:
|
|
131
|
+
for c in n.covers:
|
|
132
|
+
out.setdefault(c, []).append("not_carried")
|
|
133
|
+
return out
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sp2react
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-agent pipeline that converts classic SharePoint ASPX/jQuery list forms and pages into a React + FastAPI app on your existing database
|
|
5
|
+
Author: Karthikeyan Balasundaram
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/karthikbd/sp2react
|
|
8
|
+
Project-URL: Source Code, https://github.com/karthikbd/sp2react
|
|
9
|
+
Project-URL: Repository, https://github.com/karthikbd/sp2react
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/karthikbd/sp2react/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/karthikbd/sp2react/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Documentation, https://github.com/karthikbd/sp2react#readme
|
|
13
|
+
Keywords: sharepoint,sharepoint migration,react,fastapi,jquery,spservices,aspx,list forms,migration,code conversion,ai agents,claude
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pydantic>=2.5
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Requires-Dist: requests>=2.31
|
|
31
|
+
Provides-Extra: ntlm
|
|
32
|
+
Requires-Dist: requests-ntlm>=1.2; extra == "ntlm"
|
|
33
|
+
Provides-Extra: db
|
|
34
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "db"
|
|
35
|
+
Provides-Extra: mysql
|
|
36
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "mysql"
|
|
37
|
+
Requires-Dist: pymysql>=1.1; extra == "mysql"
|
|
38
|
+
Provides-Extra: mssql
|
|
39
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "mssql"
|
|
40
|
+
Requires-Dist: pyodbc>=5.0; extra == "mssql"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
43
|
+
Requires-Dist: fastapi>=0.110; extra == "dev"
|
|
44
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
45
|
+
Requires-Dist: uvicorn>=0.27; extra == "dev"
|
|
46
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# sp2react
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/sp2react/)
|
|
52
|
+
[](https://pypi.org/project/sp2react/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/karthikbd/sp2react)
|
|
55
|
+
|
|
56
|
+
Convert classic SharePoint (2013 / 2016 / 2019 / Subscription Edition) list forms and pages built with **ASPX, jQuery and SPServices** into a **React + FastAPI** app that runs on **your existing database**.
|
|
57
|
+
|
|
58
|
+
Plain code does everything mechanical. AI agents (Claude through your GitHub Copilot **Bridger** by default) do the parts that need judgement. Nothing an agent writes is accepted until it passes automatic checks.
|
|
59
|
+
|
|
60
|
+
**No business data over HTTP:** in the default `database` mode, list items, attachments and people are never read from SharePoint, and every request is checked against the hosts in your config.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Table of Contents
|
|
65
|
+
|
|
66
|
+
- [Installation](#installation)
|
|
67
|
+
- [Quick Start](#quick-start)
|
|
68
|
+
- [Configuration](#configuration)
|
|
69
|
+
- [The ten stages](#the-ten-stages)
|
|
70
|
+
- [Your existing database](#your-existing-database)
|
|
71
|
+
- [The generated app](#the-generated-app)
|
|
72
|
+
- [Use it from your own tool](#use-it-from-your-own-tool)
|
|
73
|
+
- [What converts and what does not](#what-converts-and-what-does-not)
|
|
74
|
+
- [Running Tests](#running-tests)
|
|
75
|
+
- [CI / CD](#ci--cd)
|
|
76
|
+
- [Security](#security)
|
|
77
|
+
- [Changelog](#changelog)
|
|
78
|
+
- [License](#license)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install "sp2react[mysql]" # MySQL / MariaDB tables
|
|
86
|
+
pip install "sp2react[mssql]" # SQL Server tables
|
|
87
|
+
pip install "sp2react[ntlm]" # add for on-premises Windows authentication
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Python 3.10+. Node.js 20+ is needed to build the generated React app.
|
|
91
|
+
|
|
92
|
+
## Quick Start
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sp2react init # writes sp2react.yaml
|
|
96
|
+
export SP_USERNAME='CORP\me' SP_PASSWORD='...' # or SP_COOKIE / SP_TOKEN
|
|
97
|
+
export BRIDGER_TOKEN='...' # only if Bridger needs one
|
|
98
|
+
export SP2REACT_DB_URL='mysql+pymysql://user:password@db-host:3306/sea'
|
|
99
|
+
|
|
100
|
+
sp2react doctor # checks SharePoint, lists Bridger's Claude models
|
|
101
|
+
sp2react run --page /sites/SEA/SitePages/Dashboard.aspx --until spec
|
|
102
|
+
# review sp2react-work/specs/<screen>.coverage.md, then:
|
|
103
|
+
sp2react approve Dashboard
|
|
104
|
+
sp2react map-db # drafts db-mapping.json; review db-mapping.md
|
|
105
|
+
sp2react run --page /sites/SEA/SitePages/Dashboard.aspx --offline
|
|
106
|
+
sp2react status
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
sharepoint:
|
|
113
|
+
site_url: https://sharepoint.example.com/sites/SEA
|
|
114
|
+
auth: ntlm # ntlm | cookie | bearer | none ; secrets come from environment variables
|
|
115
|
+
llm:
|
|
116
|
+
provider: bridger # bridger | anthropic | openai | azure_openai
|
|
117
|
+
api_format: anthropic # how Bridger serves Claude: anthropic (/v1/messages) or openai (/v1/chat/completions)
|
|
118
|
+
base_url: http://localhost:4141
|
|
119
|
+
model: claude-sonnet-4 # exact id from `sp2react doctor`
|
|
120
|
+
api_key_env: BRIDGER_TOKEN
|
|
121
|
+
pipeline:
|
|
122
|
+
data_source: database # database | sqlite | sharepoint
|
|
123
|
+
max_concurrency: 4
|
|
124
|
+
max_fix_iterations: 5
|
|
125
|
+
database:
|
|
126
|
+
url_env: SP2REACT_DB_URL
|
|
127
|
+
mapping: ./db-mapping.json
|
|
128
|
+
security:
|
|
129
|
+
allowed_hosts: []
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## The ten stages
|
|
133
|
+
|
|
134
|
+
Each stage starts only after the previous one has finished.
|
|
135
|
+
|
|
136
|
+
| # | Stage | Who | What happens | Checked by |
|
|
137
|
+
|---|---|---|---|---|
|
|
138
|
+
| 1 | inputs | code | config and connectivity | |
|
|
139
|
+
| 2 | extract | code | `.aspx` source, browser-added web parts, Script Editor / Content Editor content, JSLink, custom JS/CSS, list schemas, views, forms, group names, navigation. GET only | manifest of fetched files and errors |
|
|
140
|
+
| 3 | inventory | code | groups each list's New/Edit/Display forms into one screen and finds every jQuery behaviour and data call, with file:line | deterministic |
|
|
141
|
+
| 4 | spec | **agent** | builds the base spec from the schema, then the agent adds the rules that need interpretation | **coverage gate**: every required behaviour covered, legacy texts verbatim, rules valid |
|
|
142
|
+
| 5 | approve_spec | you | read `specs/<screen>.coverage.md` | |
|
|
143
|
+
| 6 | convert | code + **agents** | writes the fixed scaffold in seconds. List forms are rendered by the shared form engine from the spec; agents write only page components and custom handlers, in parallel | import whitelist, no fetch/jQuery/innerHTML, legacy texts present, `tsc` |
|
|
144
|
+
| 7 | review | **agent** | independent parity review | findings fed to fix |
|
|
145
|
+
| 8 | test | code | generated API tests, rule vectors (browser and API engines must agree), `tsc` + `vite build`, data reconciliation | |
|
|
146
|
+
| 9 | fix | **agent** | gets the real compiler and test output and edits only agent-owned files; up to N rounds | re-test |
|
|
147
|
+
| 10 | approve | you | | |
|
|
148
|
+
|
|
149
|
+
## Your existing database
|
|
150
|
+
|
|
151
|
+
If your team has already moved the list data into MySQL or SQL Server, the generated FastAPI app reads and writes those tables directly.
|
|
152
|
+
|
|
153
|
+
1. `sp2react map-db` reads the database **schema only** (tables, columns, types, keys).
|
|
154
|
+
2. It drafts the mapping: each list to a table, each field to a column, lookups to foreign keys, people to a users table or login column, plus created/modified/author/editor/version columns and attachments.
|
|
155
|
+
3. The `dbmap` agent resolves what the draft could not, for example `Countries.Title` migrated as `countries.country_name`.
|
|
156
|
+
4. Validation blocks code generation until every table and column exists and every required field is mapped.
|
|
157
|
+
5. You review `db-mapping.md`, and can edit `db-mapping.json` directly.
|
|
158
|
+
|
|
159
|
+
At runtime, OData-style queries from the forms and pages (`$filter`, `$orderby`, `$top`, lookup paths such as `Country/Title`) are translated to SQL. Optimistic concurrency uses your version column if you have one, otherwise the modified timestamp.
|
|
160
|
+
|
|
161
|
+
To check the earlier migration, set `security.sharepoint_item_reads: true`. `reconciliation.md` then compares every list item with its row, field by field, including orphaned lookups and attachment checksums.
|
|
162
|
+
|
|
163
|
+
## The generated app
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd converted-app/backend && pip install -r requirements.txt pymysql && cp .env.example .env && uvicorn app.main:app
|
|
167
|
+
cd converted-app/frontend && npm install && npm run dev
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- Behaviour comes from the spec at runtime. `runtime/rules.ts` (browser) and `app/rules.py` (API) interpret the same rules, and shared vectors keep them in agreement, so a direct API call cannot bypass a rule.
|
|
171
|
+
- The form uses SharePoint's layout, labels and control titles, the legacy site CSS is carried over, and old URLs (`/Lists/X/EditForm.aspx?ID=3&Source=...`) redirect to the new routes.
|
|
172
|
+
- SharePoint groups become roles. Membership comes from your users/groups tables or `data/roles.json`.
|
|
173
|
+
- Authentication: `AUTH_MODE=dev` for local work; `header` behind your gateway, or Entra ID in `app/auth.py`.
|
|
174
|
+
|
|
175
|
+
## Use it from your own tool
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from sp2react import Pipeline, load_config
|
|
179
|
+
|
|
180
|
+
p = Pipeline(load_config("sp2react.yaml"), progress=print)
|
|
181
|
+
p.stage_inputs(offline=False)
|
|
182
|
+
p.stage_extract(pages=["SitePages/Dashboard.aspx"], offline=False)
|
|
183
|
+
screens = [s["id"] for s in p.stage_inventory(["SitePages/Dashboard.aspx"])]
|
|
184
|
+
p.stage_spec(screens)
|
|
185
|
+
p.approve("Dashboard", "spec", by="reviewer")
|
|
186
|
+
p.stage_map_db([p.load_spec(s) for s in p.approved_sids()])
|
|
187
|
+
p.stage_convert(screens)
|
|
188
|
+
p.stage_review(screens)
|
|
189
|
+
p.stage_test_and_fix(screens)
|
|
190
|
+
p.state # also written to sp2react-work/state.json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## What converts and what does not
|
|
194
|
+
|
|
195
|
+
| Converts | Needs your input, or is not converted yet |
|
|
196
|
+
|---|---|
|
|
197
|
+
| List forms (New/Edit/Display) and their jQuery/SPServices behaviour | Workflows, event receivers, compiled solutions: their code is not reachable over REST. They are flagged; export the definitions or source to convert them |
|
|
198
|
+
| Web part pages (SitePages) with Script Editor / Content Editor JavaScript | InfoPath and Power Apps forms: reported, not converted |
|
|
199
|
+
| Basic list views (columns, sort, row limit) | Views with filters and grouping, document libraries, custom master-page branding, rich grids (sort/search/select/export), modal dialogs: planned |
|
|
200
|
+
| SharePoint groups as roles, old URL redirects | Calculated columns are shown but not recomputed; managed metadata, item-level permissions, alert emails |
|
|
201
|
+
|
|
202
|
+
Run the inventory on your site first. It shows which pages and scripts were found and what each one does, before you commit to a conversion.
|
|
203
|
+
|
|
204
|
+
## Running Tests
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
python -m pytest tests/ -v
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The suite runs against `tests/fixtures/legacy-site`, a fictional Contoso procurement site with jQuery 1.12 / SPServices list forms and a REST-driven dashboard. It covers:
|
|
212
|
+
- a mock SharePoint server that rejects writes,
|
|
213
|
+
- a scripted stand-in for the model, including retry-on-feedback and give-up,
|
|
214
|
+
- a renamed-schema database standing in for an earlier migration,
|
|
215
|
+
- the network policy,
|
|
216
|
+
- all ten stages including the fix loop,
|
|
217
|
+
- a real `npm install` + `tsc` + `vite build` of the generated app.
|
|
218
|
+
|
|
219
|
+
## CI / CD
|
|
220
|
+
|
|
221
|
+
- `tests.yml`: runs the test suite on Python 3.10–3.13 on every push and pull request.
|
|
222
|
+
- `python-publish.yml`: on a `v*` tag or a published release, tests, builds and publishes to PyPI through **Trusted Publishing** (OIDC, no stored token).
|
|
223
|
+
- `scorecard.yml`: runs the OpenSSF Scorecard weekly and on every push to `main`.
|
|
224
|
+
|
|
225
|
+
## Security
|
|
226
|
+
|
|
227
|
+
See [SECURITY.md](SECURITY.md). In short:
|
|
228
|
+
- Requests go only to the configured SharePoint and model hosts, and SharePoint is read-only.
|
|
229
|
+
- In database mode no list data is read over HTTP and no business data is sent to the model.
|
|
230
|
+
- Secrets come from environment variables only.
|
|
231
|
+
|
|
232
|
+
## Changelog
|
|
233
|
+
|
|
234
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
sp2react/__init__.py,sha256=FDxCRQtOt-rap-xgysEsnr_vDa7wnlZLrosQdTUJLWA,532
|
|
2
|
+
sp2react/checks.py,sha256=tAMN1E1zeLs43UVHx_gXbPlx3R3Q_iaVinibYvM-IoI,4449
|
|
3
|
+
sp2react/cli.py,sha256=_6LWDY6RuM9UG-Rgvhrbd96_OgBD721nZLz0CZePDXc,7807
|
|
4
|
+
sp2react/config.py,sha256=JzKJYv9vu5qu0L30cKkMKG3CNl_Xn5zi-m7meHY0mTY,8231
|
|
5
|
+
sp2react/dbmap.py,sha256=ePbC6SqIw23QLQV-FaLKfbeBsRQidIOc4mGC1Mqc668,13008
|
|
6
|
+
sp2react/migrate.py,sha256=Hwz6FEJOz4STgZL5t0ZNFSdoMNdJF1enmkJVZQC1Tm8,12965
|
|
7
|
+
sp2react/net.py,sha256=AXb_KYGObXqFMGMho3ujUXRqky3ce0U6mLQ9iUlnSJg,2173
|
|
8
|
+
sp2react/pipeline.py,sha256=-9dFY2X6rew_PsiGOxayJ6cL3v9JZS9wHXOcSCVwb48,32489
|
|
9
|
+
sp2react/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
sp2react/agents/core.py,sha256=I5aXzWZ_P0JKz8F_FdgS7eznGfed4PGU6QHB19t9_1Y,6491
|
|
11
|
+
sp2react/agents/tasks.py,sha256=jskE3DHZYyYsbihTTDD91LfcxWqPcFo7H5sBH5lW4LI,18957
|
|
12
|
+
sp2react/agents/prompts/dbmap.md,sha256=ueAf3oF1-aCy6R0o9nCb2wGlYFF-tmZm4aA8BMSKoQk,2196
|
|
13
|
+
sp2react/agents/prompts/fix.md,sha256=quMBtSHJBNN1yrV9zu1DVNt0St5p3wJk_C2LxzawRJc,1052
|
|
14
|
+
sp2react/agents/prompts/handler.md,sha256=MfkEBI_tlgsHG9mxd6FhzJDg58bQcydatbcV0AnoZ5A,1595
|
|
15
|
+
sp2react/agents/prompts/page.md,sha256=KpsgI16t89VwC1PBX2p2EWKuwkTT_FsuvinO2KBqfbk,3941
|
|
16
|
+
sp2react/agents/prompts/review.md,sha256=Wz8DQ39Py3CNEiunlS14XVHjdvREbZVZqpQ-ZCnR6ok,1527
|
|
17
|
+
sp2react/agents/prompts/spec.md,sha256=y4LHQ3qkF3zXuRXsdHdxMc6pk4DLBcw8QDVnxjmHy3g,8076
|
|
18
|
+
sp2react/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
sp2react/analysis/aspx.py,sha256=ru70jioqpjU63I5M_UpQPHQnM1CrQ-OLaBbhJRrqvC0,11633
|
|
20
|
+
sp2react/analysis/behaviors.py,sha256=EJhq631aik0siqLCX1rAQ6iqJwcw-7shLcQ7o0oNBWo,17577
|
|
21
|
+
sp2react/analysis/fields.py,sha256=khJB3aV-KDCJBaRG-Zlj69rk_aj7dMfZBHoEck_KODE,8001
|
|
22
|
+
sp2react/analysis/inventory.py,sha256=EhFRiDWwps39jvmDO8X8FPMT8kcoC5g34hoZcwgH1DI,16976
|
|
23
|
+
sp2react/analysis/jsscan.py,sha256=y2ziAQb6Oy-U-MknNtxp8YfGpFq0D3HX2pVaSnqFS6I,12064
|
|
24
|
+
sp2react/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
sp2react/llm/providers.py,sha256=u7w3hw1xSJdPSv_ESWChagk1bWbsWoN6pmo-3WNHDIQ,9557
|
|
26
|
+
sp2react/scaffold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
sp2react/scaffold/generate.py,sha256=m29FjkidbI1iQdt_kCP6bU5XwOJwno4BpwMR2N10IKc,13752
|
|
28
|
+
sp2react/scaffold/rule_vectors.json,sha256=wc6qbyH-01hXlwLwInuPhvnsaxkjRr6znEeZZF8O__U,65256
|
|
29
|
+
sp2react/scaffold/templates/backend/requirements.txt,sha256=Hyyb3onxzWHnfJdrZoBg2vgxijxjFXnPeFPARuXG2Ew,338
|
|
30
|
+
sp2react/scaffold/templates/backend/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
sp2react/scaffold/templates/backend/app/api.py,sha256=6EiiBRu-NHxs37jBf_hh2h37NZNoDsrAStbBhwqpc14,14509
|
|
32
|
+
sp2react/scaffold/templates/backend/app/auth.py,sha256=Cm7y47iANYeYUPRTljD-YDUczcILwyMTmJWxxT5mqg8,1594
|
|
33
|
+
sp2react/scaffold/templates/backend/app/main.py,sha256=yZoj_SGFw9zeVd1ttcfKN5B5XlBp9-uAxfZ1E3apNU8,1805
|
|
34
|
+
sp2react/scaffold/templates/backend/app/odata.py,sha256=kUR5Dmp8bIV10NV6gmW2zmQSpCFsIp8qvTXr15lVIes,6469
|
|
35
|
+
sp2react/scaffold/templates/backend/app/rules.py,sha256=cf8528TJgqIkg8inW7585Z1vYIJp0rEKz5hAUikaDM0,11093
|
|
36
|
+
sp2react/scaffold/templates/backend/app/sp_repo.py,sha256=U9jKo45uTCpopb5Rj9R3Gl27iQjVOUoUEgrK6yMz8WQ,8103
|
|
37
|
+
sp2react/scaffold/templates/backend/app/sql_repo.py,sha256=KG_fau-o69FthBvQeQwH5a2S0nAJIwnXsroYzJ7NU04,20247
|
|
38
|
+
sp2react/scaffold/templates/backend/app/store.py,sha256=JJGSpgwdmvop1n9UCoeUz6pgnWSZ3LeE_oHuVx5NTnY,11081
|
|
39
|
+
sp2react/scaffold/templates/backend/app/custom/__init__.py,sha256=TC05h9ehKDLJhkVyN1wnYAEke_cBsMNQ7_n8D2Hd8vs,820
|
|
40
|
+
sp2react/scaffold/templates/backend/tests/conftest.py,sha256=6WLrWWAgC4uzdKx0-Hh4qQH_U2zo7vu5ir4lWBWYKlo,250
|
|
41
|
+
sp2react/scaffold/templates/backend/tests/test_rule_vectors.py,sha256=eYVbk6Umm7RRTfpHXyS4rR-IIogdX9UKfrq9DqT4lpQ,1553
|
|
42
|
+
sp2react/scaffold/templates/frontend/index.html,sha256=hIgnzooYwTxIWRVCRA8BIeyLxM1mBK498aUqKtBMB-Y,301
|
|
43
|
+
sp2react/scaffold/templates/frontend/package.json,sha256=UvxQzmf0vZRSrdg06KYE2sTnEeJLlwFDTCkSVt7N9UQ,541
|
|
44
|
+
sp2react/scaffold/templates/frontend/tsconfig.json,sha256=wQo9WmOb56jFINECi85gir9AzEF5rutk6Lh0MOfbgbQ,434
|
|
45
|
+
sp2react/scaffold/templates/frontend/vite.config.ts,sha256=rMa1S-svNgrZ21HH9W9FHhwt3p7yBhU58gG2BhEstHc,206
|
|
46
|
+
sp2react/scaffold/templates/frontend/scripts/check-rules.ts,sha256=x5lo3ljK9Lo4rcVugHeZzBNLw9GBNkVPPeLTl8lh46w,1767
|
|
47
|
+
sp2react/scaffold/templates/frontend/src/api.ts,sha256=U3BJjMq0allELxdP7TBD2QuwA4ZkV-9QPDSqAiuZp2s,4107
|
|
48
|
+
sp2react/scaffold/templates/frontend/src/main.tsx,sha256=vumNKICXxEFIkTRVYdd_XIN9nZYR_t5jsnjVINr46t0,363
|
|
49
|
+
sp2react/scaffold/templates/frontend/src/vite-env.d.ts,sha256=ZZlpNvuwQpFfe3SiAPzd5-QQ8ypmmxq5WXz6pLD63bU,38
|
|
50
|
+
sp2react/scaffold/templates/frontend/src/components/sp/Banner.tsx,sha256=5KMNGO2tdh8rfVP_WGRojBRc5OlTLBnLIIVctiR6R8c,252
|
|
51
|
+
sp2react/scaffold/templates/frontend/src/components/sp/DataTable.tsx,sha256=VQaEsqgjkYwjKBRB9aSi3kQQYUQK7EJup-Fgb-wspnc,1106
|
|
52
|
+
sp2react/scaffold/templates/frontend/src/components/sp/Form.tsx,sha256=8QaTVlXdZep2Oa5Ykg3IQ13OrkZkbhzK60nte752Q0M,12560
|
|
53
|
+
sp2react/scaffold/templates/frontend/src/components/sp/GenericPage.tsx,sha256=r4jD45g4j06xxT3gwcOMJWG9xJGcmEWF4Ki5pElnI8k,1350
|
|
54
|
+
sp2react/scaffold/templates/frontend/src/components/sp/Layout.tsx,sha256=2ZKUp0v83o0t_uvXYc8pxxqi_uIKMbn3DzbcgAlEAow,2301
|
|
55
|
+
sp2react/scaffold/templates/frontend/src/components/sp/LegacyHtml.tsx,sha256=BgbDXCSri_TXvoXmDYaDFWtjtlEgSbivUr7RW-nFeIU,569
|
|
56
|
+
sp2react/scaffold/templates/frontend/src/components/sp/ListView.tsx,sha256=womt8IiNIWnsTIJLyb5ug4px9VSAvhKiq_2sOwa0nj0,3060
|
|
57
|
+
sp2react/scaffold/templates/frontend/src/components/sp/Tiles.tsx,sha256=0LFWzZRgDhJFjeYEH6ygZ-oouG7h6o_JWW-xq3Ig0zY,517
|
|
58
|
+
sp2react/scaffold/templates/frontend/src/components/sp/fields.tsx,sha256=SX4f3K8_uz_LIJgCYAjkXpc7NddIn6gx5uD8Fwc9URI,5825
|
|
59
|
+
sp2react/scaffold/templates/frontend/src/custom/types.ts,sha256=EGUCGkK2vhvntJnv2KDYSXT7VSbAD_9wblHDI6s_ei4,399
|
|
60
|
+
sp2react/scaffold/templates/frontend/src/runtime/format.ts,sha256=BUVjOPWUXQmDrfDGYRuU7TfyPQz3tGlu-XcgdpdesMg,1550
|
|
61
|
+
sp2react/scaffold/templates/frontend/src/runtime/rules.ts,sha256=g4WqMMoykZ_CGkaLmHI68YRFI2a5nFyqbXKmwyFbouA,10413
|
|
62
|
+
sp2react/scaffold/templates/frontend/src/runtime/spec.ts,sha256=0SGKcuz5kc8jloLQlz8Utw6vRYaBxe0OpyQcdj9Vqm0,2582
|
|
63
|
+
sp2react/scaffold/templates/frontend/src/runtime/user.tsx,sha256=5RnkR-txQZXotBYCk7GESTXL3r2YCC3eQMyR1om0g98,913
|
|
64
|
+
sp2react/scaffold/templates/frontend/src/screens/LegacyRedirect.tsx,sha256=S5O29r9XEnkxpsoWV8BeU8E1yLkZs0FwRhejfqb1EXU,1478
|
|
65
|
+
sp2react/scaffold/templates/frontend/src/screens/ListFormScreen.tsx,sha256=ymOuMZZb37YLpXTEyyEPoCCP8qUSaMHYaBRfKUEpEQ0,1679
|
|
66
|
+
sp2react/scaffold/templates/frontend/src/styles/sp.css,sha256=pvJpMrRDt6LbGGZ7D0rnDZUnDlDkIvZzv2FR_VTuw_Q,5846
|
|
67
|
+
sp2react/sharepoint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
+
sp2react/sharepoint/client.py,sha256=86WzL05rM4wKadNKjXv4OAuNy-Ob-l7JZ6CIZpPv51w,9956
|
|
69
|
+
sp2react/sharepoint/extract.py,sha256=mnONIJnnz8xqoprGTS1Q30vS2oCLJwjWqvwMThZ8ESI,11145
|
|
70
|
+
sp2react/spec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
|
+
sp2react/spec/base.py,sha256=V2zJEsUVgCv8ZsYNG4C2taEMFNkowj1WyPL3IMYDf-U,4318
|
|
72
|
+
sp2react/spec/coverage.py,sha256=T0OVvjsHd3W8uzu89tOhJnDzu-FrN2QUyUA9LvrplgM,8940
|
|
73
|
+
sp2react/spec/model.py,sha256=k1KJ-4PlauNdO6Mp8bF9f-nmyZB_nIOHgI7pINorL4E,5513
|
|
74
|
+
sp2react-0.1.0.dist-info/licenses/LICENSE,sha256=nLfpXwI9bP5mzI3n2TobXSQt1XKbWbvaxB-qhq18Qug,1081
|
|
75
|
+
sp2react-0.1.0.dist-info/METADATA,sha256=su2JD9pqmNkgwV0GZ155vspkX2wwOtX0Y_gvgFjCtK4,11967
|
|
76
|
+
sp2react-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
77
|
+
sp2react-0.1.0.dist-info/entry_points.txt,sha256=48Bk0upTdoLUd8sn4KG5l1peCWUIA2nHUVuQmwwS-Sc,47
|
|
78
|
+
sp2react-0.1.0.dist-info/top_level.txt,sha256=vMnbbD0OZPkMRiU6LKseJhEUlMgfx9HSJRdQvyPAlWg,9
|
|
79
|
+
sp2react-0.1.0.dist-info/RECORD,,
|