pcf-toolkit 0.2.5__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.
- pcf_toolkit/__init__.py +6 -0
- pcf_toolkit/cli.py +738 -0
- pcf_toolkit/cli_helpers.py +62 -0
- pcf_toolkit/data/__init__.py +1 -0
- pcf_toolkit/data/manifest.schema.json +1097 -0
- pcf_toolkit/data/schema_snapshot.json +2377 -0
- pcf_toolkit/data/spec_raw.json +2877 -0
- pcf_toolkit/io.py +65 -0
- pcf_toolkit/json_schema.py +30 -0
- pcf_toolkit/models.py +384 -0
- pcf_toolkit/proxy/__init__.py +1 -0
- pcf_toolkit/proxy/addons/__init__.py +1 -0
- pcf_toolkit/proxy/addons/redirect_bundle.py +70 -0
- pcf_toolkit/proxy/browser.py +157 -0
- pcf_toolkit/proxy/cli.py +1570 -0
- pcf_toolkit/proxy/config.py +310 -0
- pcf_toolkit/proxy/doctor.py +279 -0
- pcf_toolkit/proxy/mitm.py +206 -0
- pcf_toolkit/proxy/server.py +50 -0
- pcf_toolkit/py.typed +1 -0
- pcf_toolkit/rich_help.py +173 -0
- pcf_toolkit/schema_snapshot.py +47 -0
- pcf_toolkit/types.py +95 -0
- pcf_toolkit/xml.py +484 -0
- pcf_toolkit/xml_import.py +548 -0
- pcf_toolkit-0.2.5.dist-info/METADATA +494 -0
- pcf_toolkit-0.2.5.dist-info/RECORD +31 -0
- pcf_toolkit-0.2.5.dist-info/WHEEL +5 -0
- pcf_toolkit-0.2.5.dist-info/entry_points.txt +2 -0
- pcf_toolkit-0.2.5.dist-info/licenses/LICENSE.md +183 -0
- pcf_toolkit-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Mitmproxy bootstrap helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class MitmproxyInstall:
|
|
15
|
+
binary: Path
|
|
16
|
+
venv_dir: Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def managed_venv_dir() -> Path:
|
|
20
|
+
"""Returns the path to the managed mitmproxy virtual environment.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to ~/.pcf-toolkit/venvs/mitmproxy.
|
|
24
|
+
"""
|
|
25
|
+
return Path.home() / ".pcf-toolkit" / "venvs" / "mitmproxy"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_mitmproxy(explicit_path: Path | None = None) -> Path | None:
|
|
29
|
+
"""Finds mitmproxy executable.
|
|
30
|
+
|
|
31
|
+
Searches system PATH and managed venv.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
explicit_path: Explicit path to mitmproxy binary.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to mitmproxy executable, or None if not found.
|
|
38
|
+
"""
|
|
39
|
+
if explicit_path and explicit_path.exists():
|
|
40
|
+
return explicit_path
|
|
41
|
+
|
|
42
|
+
for candidate in ("mitmdump", "mitmproxy"):
|
|
43
|
+
resolved = shutil.which(candidate)
|
|
44
|
+
if resolved:
|
|
45
|
+
return Path(resolved)
|
|
46
|
+
|
|
47
|
+
venv_dir = managed_venv_dir()
|
|
48
|
+
for candidate in ("mitmdump", "mitmproxy"):
|
|
49
|
+
resolved = _venv_bin_path(venv_dir, candidate)
|
|
50
|
+
if resolved and resolved.exists():
|
|
51
|
+
return resolved
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ensure_mitmproxy(auto_install: bool, explicit_path: Path | None = None) -> Path:
|
|
57
|
+
"""Ensures mitmproxy is available, installing if needed.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
auto_install: If True, installs mitmproxy if not found.
|
|
61
|
+
explicit_path: Explicit path to mitmproxy binary.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Path to mitmproxy executable.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
FileNotFoundError: If mitmproxy not found and auto_install is False.
|
|
68
|
+
"""
|
|
69
|
+
existing = find_mitmproxy(explicit_path)
|
|
70
|
+
if existing:
|
|
71
|
+
return existing
|
|
72
|
+
if not auto_install:
|
|
73
|
+
raise FileNotFoundError("mitmproxy not found")
|
|
74
|
+
install = install_mitmproxy(managed_venv_dir())
|
|
75
|
+
return install.binary
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def install_mitmproxy(venv_dir: Path) -> MitmproxyInstall:
|
|
79
|
+
"""Installs mitmproxy into a virtual environment.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
venv_dir: Directory for the virtual environment.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
MitmproxyInstall instance with binary and venv paths.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
FileNotFoundError: If installation fails to produce a binary.
|
|
89
|
+
"""
|
|
90
|
+
venv_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
python_bin = _venv_python(venv_dir)
|
|
92
|
+
if not python_bin.exists():
|
|
93
|
+
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
|
|
94
|
+
python_bin = _venv_python(venv_dir)
|
|
95
|
+
_ensure_pip(python_bin)
|
|
96
|
+
subprocess.run(
|
|
97
|
+
[
|
|
98
|
+
str(python_bin),
|
|
99
|
+
"-m",
|
|
100
|
+
"pip",
|
|
101
|
+
"install",
|
|
102
|
+
"--upgrade",
|
|
103
|
+
"pip",
|
|
104
|
+
],
|
|
105
|
+
check=True,
|
|
106
|
+
)
|
|
107
|
+
subprocess.run([str(python_bin), "-m", "pip", "install", "mitmproxy"], check=True)
|
|
108
|
+
mitm_binary = _venv_bin_path(venv_dir, "mitmdump")
|
|
109
|
+
if mitm_binary is None or not mitm_binary.exists():
|
|
110
|
+
mitm_binary = _venv_bin_path(venv_dir, "mitmproxy")
|
|
111
|
+
if mitm_binary is None:
|
|
112
|
+
raise FileNotFoundError("mitmproxy install did not produce a binary")
|
|
113
|
+
return MitmproxyInstall(binary=mitm_binary, venv_dir=venv_dir)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def spawn_mitmproxy(
|
|
117
|
+
binary: Path,
|
|
118
|
+
addon_path: Path,
|
|
119
|
+
host: str,
|
|
120
|
+
port: int,
|
|
121
|
+
env: dict[str, str],
|
|
122
|
+
stdout=None,
|
|
123
|
+
stderr=None,
|
|
124
|
+
start_new_session: bool = False,
|
|
125
|
+
creationflags: int = 0,
|
|
126
|
+
) -> subprocess.Popen:
|
|
127
|
+
"""Spawns a mitmproxy process with the redirect addon.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
binary: Path to mitmproxy executable.
|
|
131
|
+
addon_path: Path to the redirect addon script.
|
|
132
|
+
host: Hostname to listen on.
|
|
133
|
+
port: Port to listen on.
|
|
134
|
+
env: Environment variables to pass to the process.
|
|
135
|
+
stdout: Standard output handle (optional).
|
|
136
|
+
stderr: Standard error handle (optional).
|
|
137
|
+
start_new_session: If True, starts process in new session.
|
|
138
|
+
creationflags: Windows process creation flags.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Popen instance for the mitmproxy process.
|
|
142
|
+
"""
|
|
143
|
+
cmd = [
|
|
144
|
+
str(binary),
|
|
145
|
+
"-s",
|
|
146
|
+
str(addon_path),
|
|
147
|
+
"--listen-host",
|
|
148
|
+
host,
|
|
149
|
+
"--listen-port",
|
|
150
|
+
str(port),
|
|
151
|
+
]
|
|
152
|
+
return subprocess.Popen(
|
|
153
|
+
cmd,
|
|
154
|
+
env=env,
|
|
155
|
+
stdout=stdout,
|
|
156
|
+
stderr=stderr,
|
|
157
|
+
start_new_session=start_new_session,
|
|
158
|
+
creationflags=creationflags,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _ensure_pip(python_bin: Path) -> None:
|
|
163
|
+
"""Ensures pip is available in the Python installation.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
python_bin: Path to Python executable.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
subprocess.run(
|
|
170
|
+
[str(python_bin), "-m", "pip", "--version"],
|
|
171
|
+
check=True,
|
|
172
|
+
stdout=subprocess.DEVNULL,
|
|
173
|
+
stderr=subprocess.DEVNULL,
|
|
174
|
+
)
|
|
175
|
+
except subprocess.CalledProcessError:
|
|
176
|
+
subprocess.run([str(python_bin), "-m", "ensurepip"], check=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _venv_python(venv_dir: Path) -> Path:
|
|
180
|
+
"""Returns the path to Python executable in a virtual environment.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
venv_dir: Virtual environment directory.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Path to Python executable.
|
|
187
|
+
"""
|
|
188
|
+
if os.name == "nt":
|
|
189
|
+
return venv_dir / "Scripts" / "python.exe"
|
|
190
|
+
return venv_dir / "bin" / "python"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _venv_bin_path(venv_dir: Path, name: str) -> Path | None:
|
|
194
|
+
"""Returns the path to an executable in a virtual environment.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
venv_dir: Virtual environment directory.
|
|
198
|
+
name: Executable name.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Path to executable, or None if venv structure is invalid.
|
|
202
|
+
"""
|
|
203
|
+
bin_dir = venv_dir / ("Scripts" if os.name == "nt" else "bin")
|
|
204
|
+
if os.name == "nt":
|
|
205
|
+
return bin_dir / f"{name}.exe"
|
|
206
|
+
return bin_dir / name
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Local file server helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def spawn_http_server(
|
|
11
|
+
directory: Path,
|
|
12
|
+
host: str,
|
|
13
|
+
port: int,
|
|
14
|
+
stdout=None,
|
|
15
|
+
stderr=None,
|
|
16
|
+
start_new_session: bool = False,
|
|
17
|
+
creationflags: int = 0,
|
|
18
|
+
) -> subprocess.Popen:
|
|
19
|
+
"""Spawns a local HTTP server process.
|
|
20
|
+
|
|
21
|
+
Uses Python's built-in http.server module.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
directory: Directory to serve files from.
|
|
25
|
+
host: Hostname to bind to.
|
|
26
|
+
port: Port to listen on.
|
|
27
|
+
stdout: Standard output handle (optional).
|
|
28
|
+
stderr: Standard error handle (optional).
|
|
29
|
+
start_new_session: If True, starts process in new session.
|
|
30
|
+
creationflags: Windows process creation flags.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Popen instance for the HTTP server process.
|
|
34
|
+
"""
|
|
35
|
+
cmd = [
|
|
36
|
+
sys.executable,
|
|
37
|
+
"-m",
|
|
38
|
+
"http.server",
|
|
39
|
+
str(port),
|
|
40
|
+
"--bind",
|
|
41
|
+
host,
|
|
42
|
+
]
|
|
43
|
+
return subprocess.Popen(
|
|
44
|
+
cmd,
|
|
45
|
+
cwd=directory,
|
|
46
|
+
stdout=stdout,
|
|
47
|
+
stderr=stderr,
|
|
48
|
+
start_new_session=start_new_session,
|
|
49
|
+
creationflags=creationflags,
|
|
50
|
+
)
|
pcf_toolkit/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
pcf_toolkit/rich_help.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Custom Rich help formatting for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import typer.core as core
|
|
9
|
+
import typer.rich_utils as rich_utils
|
|
10
|
+
from rich.align import Align
|
|
11
|
+
from rich.padding import Padding
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rich_format_help_custom(
|
|
16
|
+
*,
|
|
17
|
+
obj: click.Command | click.Group,
|
|
18
|
+
ctx: click.Context,
|
|
19
|
+
markup_mode: rich_utils.MarkupModeStrict,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Prints Rich-formatted help with a custom examples panel.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
obj: Click command or group to format help for.
|
|
25
|
+
ctx: Click context.
|
|
26
|
+
markup_mode: Rich markup mode for formatting.
|
|
27
|
+
"""
|
|
28
|
+
console = rich_utils._get_rich_console()
|
|
29
|
+
|
|
30
|
+
# Usage
|
|
31
|
+
console.print(
|
|
32
|
+
Padding(rich_utils.highlighter(obj.get_usage(ctx)), 1),
|
|
33
|
+
style=rich_utils.STYLE_USAGE_COMMAND,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Main help text
|
|
37
|
+
if obj.help:
|
|
38
|
+
console.print(
|
|
39
|
+
Padding(
|
|
40
|
+
Align(
|
|
41
|
+
rich_utils._get_help_text(
|
|
42
|
+
obj=obj,
|
|
43
|
+
markup_mode=markup_mode,
|
|
44
|
+
),
|
|
45
|
+
pad=False,
|
|
46
|
+
),
|
|
47
|
+
(0, 1, 1, 1),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list)
|
|
52
|
+
panel_to_options: defaultdict[str, list[click.Option]] = defaultdict(list)
|
|
53
|
+
|
|
54
|
+
for param in obj.get_params(ctx):
|
|
55
|
+
if getattr(param, "hidden", False):
|
|
56
|
+
continue
|
|
57
|
+
if isinstance(param, click.Argument):
|
|
58
|
+
panel_name = getattr(param, rich_utils._RICH_HELP_PANEL_NAME, None) or rich_utils.ARGUMENTS_PANEL_TITLE
|
|
59
|
+
panel_to_arguments[panel_name].append(param)
|
|
60
|
+
elif isinstance(param, click.Option):
|
|
61
|
+
panel_name = getattr(param, rich_utils._RICH_HELP_PANEL_NAME, None) or rich_utils.OPTIONS_PANEL_TITLE
|
|
62
|
+
panel_to_options[panel_name].append(param)
|
|
63
|
+
|
|
64
|
+
default_arguments = panel_to_arguments.get(rich_utils.ARGUMENTS_PANEL_TITLE, [])
|
|
65
|
+
rich_utils._print_options_panel(
|
|
66
|
+
name=rich_utils.ARGUMENTS_PANEL_TITLE,
|
|
67
|
+
params=default_arguments,
|
|
68
|
+
ctx=ctx,
|
|
69
|
+
markup_mode=markup_mode,
|
|
70
|
+
console=console,
|
|
71
|
+
)
|
|
72
|
+
for panel_name, arguments in panel_to_arguments.items():
|
|
73
|
+
if panel_name == rich_utils.ARGUMENTS_PANEL_TITLE:
|
|
74
|
+
continue
|
|
75
|
+
rich_utils._print_options_panel(
|
|
76
|
+
name=panel_name,
|
|
77
|
+
params=arguments,
|
|
78
|
+
ctx=ctx,
|
|
79
|
+
markup_mode=markup_mode,
|
|
80
|
+
console=console,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
default_options = panel_to_options.get(rich_utils.OPTIONS_PANEL_TITLE, [])
|
|
84
|
+
rich_utils._print_options_panel(
|
|
85
|
+
name=rich_utils.OPTIONS_PANEL_TITLE,
|
|
86
|
+
params=default_options,
|
|
87
|
+
ctx=ctx,
|
|
88
|
+
markup_mode=markup_mode,
|
|
89
|
+
console=console,
|
|
90
|
+
)
|
|
91
|
+
for panel_name, options in panel_to_options.items():
|
|
92
|
+
if panel_name == rich_utils.OPTIONS_PANEL_TITLE:
|
|
93
|
+
continue
|
|
94
|
+
rich_utils._print_options_panel(
|
|
95
|
+
name=panel_name,
|
|
96
|
+
params=options,
|
|
97
|
+
ctx=ctx,
|
|
98
|
+
markup_mode=markup_mode,
|
|
99
|
+
console=console,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if isinstance(obj, click.Group):
|
|
103
|
+
panel_to_commands: defaultdict[str, list[click.Command]] = defaultdict(list)
|
|
104
|
+
for command_name in obj.list_commands(ctx):
|
|
105
|
+
command = obj.get_command(ctx, command_name)
|
|
106
|
+
if command and not command.hidden:
|
|
107
|
+
panel_name = getattr(command, rich_utils._RICH_HELP_PANEL_NAME, None) or rich_utils.COMMANDS_PANEL_TITLE
|
|
108
|
+
panel_to_commands[panel_name].append(command)
|
|
109
|
+
|
|
110
|
+
max_cmd_len = max(
|
|
111
|
+
[len(command.name or "") for commands in panel_to_commands.values() for command in commands],
|
|
112
|
+
default=0,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
default_commands = panel_to_commands.get(rich_utils.COMMANDS_PANEL_TITLE, [])
|
|
116
|
+
rich_utils._print_commands_panel(
|
|
117
|
+
name=rich_utils.COMMANDS_PANEL_TITLE,
|
|
118
|
+
commands=default_commands,
|
|
119
|
+
markup_mode=markup_mode,
|
|
120
|
+
console=console,
|
|
121
|
+
cmd_len=max_cmd_len,
|
|
122
|
+
)
|
|
123
|
+
for panel_name, commands in panel_to_commands.items():
|
|
124
|
+
if panel_name == rich_utils.COMMANDS_PANEL_TITLE:
|
|
125
|
+
continue
|
|
126
|
+
rich_utils._print_commands_panel(
|
|
127
|
+
name=panel_name,
|
|
128
|
+
commands=commands,
|
|
129
|
+
markup_mode=markup_mode,
|
|
130
|
+
console=console,
|
|
131
|
+
cmd_len=max_cmd_len,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Custom epilog panel (no line collapsing)
|
|
135
|
+
if obj.epilog:
|
|
136
|
+
title = "Examples & Tips" if isinstance(obj, click.Group) else "Examples"
|
|
137
|
+
epilogue_text = rich_utils._make_rich_text(
|
|
138
|
+
text=obj.epilog,
|
|
139
|
+
markup_mode=markup_mode,
|
|
140
|
+
)
|
|
141
|
+
panel = Panel(
|
|
142
|
+
epilogue_text,
|
|
143
|
+
title=title,
|
|
144
|
+
title_align="left",
|
|
145
|
+
border_style="cyan",
|
|
146
|
+
)
|
|
147
|
+
console.print(Padding(panel, 1))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class RichTyperCommand(core.TyperCommand):
|
|
151
|
+
"""Typer command with custom Rich help output."""
|
|
152
|
+
|
|
153
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: # type: ignore[override]
|
|
154
|
+
if not core.HAS_RICH or self.rich_markup_mode is None:
|
|
155
|
+
return super().format_help(ctx, formatter)
|
|
156
|
+
return rich_format_help_custom(
|
|
157
|
+
obj=self,
|
|
158
|
+
ctx=ctx,
|
|
159
|
+
markup_mode=self.rich_markup_mode,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class RichTyperGroup(core.TyperGroup):
|
|
164
|
+
"""Typer group with custom Rich help output."""
|
|
165
|
+
|
|
166
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: # type: ignore[override]
|
|
167
|
+
if not core.HAS_RICH or self.rich_markup_mode is None:
|
|
168
|
+
return super().format_help(ctx, formatter)
|
|
169
|
+
return rich_format_help_custom(
|
|
170
|
+
obj=self,
|
|
171
|
+
ctx=ctx,
|
|
172
|
+
markup_mode=self.rich_markup_mode,
|
|
173
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Utilities for schema snapshot export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_schema_snapshot() -> str:
|
|
10
|
+
"""Loads the schema snapshot JSON as a string.
|
|
11
|
+
|
|
12
|
+
Tries to load from package data first, then falls back to local files.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The schema snapshot JSON content as a string.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
FileNotFoundError: If no schema snapshot file can be found.
|
|
19
|
+
"""
|
|
20
|
+
package_path = _read_package_snapshot()
|
|
21
|
+
if package_path is not None:
|
|
22
|
+
return package_path
|
|
23
|
+
|
|
24
|
+
file_path = Path("data/schema_snapshot.json")
|
|
25
|
+
if file_path.exists():
|
|
26
|
+
return file_path.read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
fallback = Path("data/spec_raw.json")
|
|
29
|
+
if fallback.exists():
|
|
30
|
+
return fallback.read_text(encoding="utf-8")
|
|
31
|
+
|
|
32
|
+
raise FileNotFoundError("schema snapshot not found")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_package_snapshot() -> str | None:
|
|
36
|
+
"""Reads the schema snapshot from package data.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The schema snapshot JSON content, or None if not found.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
with resources.files("pcf_toolkit.data").joinpath("schema_snapshot.json").open("r", encoding="utf-8") as handle:
|
|
43
|
+
return handle.read()
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
return None
|
|
46
|
+
except ModuleNotFoundError:
|
|
47
|
+
return None
|
pcf_toolkit/types.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Shared types and enums for PCF manifest models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ControlType(str, Enum):
|
|
9
|
+
"""Defines the control type of a PCF component."""
|
|
10
|
+
|
|
11
|
+
STANDARD = "standard"
|
|
12
|
+
VIRTUAL = "virtual"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DependencyType(str, Enum):
|
|
16
|
+
"""Defines the dependency type for a component."""
|
|
17
|
+
|
|
18
|
+
CONTROL = "control"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DependencyLoadType(str, Enum):
|
|
22
|
+
"""Defines how a dependency is loaded."""
|
|
23
|
+
|
|
24
|
+
ON_DEMAND = "onDemand"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PlatformActionType(str, Enum):
|
|
28
|
+
"""Defines platform action types."""
|
|
29
|
+
|
|
30
|
+
AFTER_PAGE_LOAD = "afterPageLoad"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PlatformLibraryName(str, Enum):
|
|
34
|
+
"""Platform library names for React controls and platform libraries."""
|
|
35
|
+
|
|
36
|
+
REACT = "React"
|
|
37
|
+
FLUENT = "Fluent"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PropertyUsage(str, Enum):
|
|
41
|
+
"""Usage values for property elements."""
|
|
42
|
+
|
|
43
|
+
BOUND = "bound"
|
|
44
|
+
INPUT = "input"
|
|
45
|
+
OUTPUT = "output"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PropertySetUsage(str, Enum):
|
|
49
|
+
"""Usage values for property-set elements."""
|
|
50
|
+
|
|
51
|
+
BOUND = "bound"
|
|
52
|
+
INPUT = "input"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RequiredFor(str, Enum):
|
|
56
|
+
"""Supported required-for values for property-dependency."""
|
|
57
|
+
|
|
58
|
+
SCHEMA = "schema"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TypeValue(str, Enum):
|
|
62
|
+
"""Supported data types for property and type elements."""
|
|
63
|
+
|
|
64
|
+
CURRENCY = "Currency"
|
|
65
|
+
DATE_AND_TIME = "DateAndTime.DateAndTime"
|
|
66
|
+
DATE_ONLY = "DateAndTime.DateOnly"
|
|
67
|
+
DECIMAL = "Decimal"
|
|
68
|
+
ENUM = "Enum"
|
|
69
|
+
FP = "FP"
|
|
70
|
+
LOOKUP_SIMPLE = "Lookup.Simple"
|
|
71
|
+
MULTIPLE = "Multiple"
|
|
72
|
+
MULTI_SELECT_OPTION_SET = "MultiSelectOptionSet"
|
|
73
|
+
OBJECT = "Object"
|
|
74
|
+
OPTION_SET = "OptionSet"
|
|
75
|
+
SINGLE_LINE_EMAIL = "SingleLine.Email"
|
|
76
|
+
SINGLE_LINE_PHONE = "SingleLine.Phone"
|
|
77
|
+
SINGLE_LINE_TEXT = "SingleLine.Text"
|
|
78
|
+
SINGLE_LINE_TEXT_AREA = "SingleLine.TextArea"
|
|
79
|
+
SINGLE_LINE_TICKER = "SingleLine.Ticker"
|
|
80
|
+
SINGLE_LINE_URL = "SingleLine.URL"
|
|
81
|
+
TWO_OPTIONS = "TwoOptions"
|
|
82
|
+
WHOLE_NONE = "Whole.None"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
UNSUPPORTED_TYPE_VALUES = {
|
|
86
|
+
"Lookup.Customer",
|
|
87
|
+
"Lookup.Owner",
|
|
88
|
+
"Lookup.PartyList",
|
|
89
|
+
"Lookup.Regarding",
|
|
90
|
+
"Status Reason",
|
|
91
|
+
"Status",
|
|
92
|
+
"Whole.Duration",
|
|
93
|
+
"Whole.Language",
|
|
94
|
+
"Whole.TimeZone",
|
|
95
|
+
}
|