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
@@ -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
+ [![PyPI version](https://badge.fury.io/py/sp2react.svg)](https://pypi.org/project/sp2react/)
52
+ [![Python versions](https://img.shields.io/pypi/pyversions/sp2react.svg)](https://pypi.org/project/sp2react/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
54
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/karthikbd/sp2react/badge)](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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sp2react = sp2react.cli:main