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.
@@ -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
+
@@ -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
+ }