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
pcf_toolkit/io.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Load manifest definitions from JSON or YAML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from pcf_toolkit.models import Manifest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_manifest(path: str) -> Manifest:
|
|
16
|
+
"""Loads a manifest definition from JSON or YAML.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path: Path to the input file, or '-' to read from stdin.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A validated Manifest instance.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValidationError: If the manifest data is invalid.
|
|
26
|
+
"""
|
|
27
|
+
data = _load_data(path)
|
|
28
|
+
return Manifest.model_validate(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_data(path: str) -> dict[str, Any]:
|
|
32
|
+
"""Loads raw data from a file or stdin.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: Path to the input file, or '-' to read from stdin.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Parsed dictionary data from JSON or YAML.
|
|
39
|
+
"""
|
|
40
|
+
if path == "-":
|
|
41
|
+
raw = sys.stdin.read()
|
|
42
|
+
return _loads_by_content(raw)
|
|
43
|
+
|
|
44
|
+
file_path = Path(path)
|
|
45
|
+
raw = file_path.read_text(encoding="utf-8")
|
|
46
|
+
if file_path.suffix.lower() in {".yaml", ".yml"}:
|
|
47
|
+
return yaml.safe_load(raw)
|
|
48
|
+
if file_path.suffix.lower() == ".json":
|
|
49
|
+
return json.loads(raw)
|
|
50
|
+
return _loads_by_content(raw)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _loads_by_content(raw: str) -> dict[str, Any]:
|
|
54
|
+
"""Parses raw string content as JSON or YAML based on content.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
raw: Raw string content to parse.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Parsed dictionary data.
|
|
61
|
+
"""
|
|
62
|
+
stripped = raw.lstrip()
|
|
63
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
64
|
+
return json.loads(raw)
|
|
65
|
+
return yaml.safe_load(raw)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Generate JSON Schema for PCF manifest YAML/JSON validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pcf_toolkit.models import Manifest
|
|
9
|
+
|
|
10
|
+
JSON_SCHEMA_URL = "https://json-schema.org/draft/2020-12/schema"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def manifest_schema() -> dict[str, Any]:
|
|
14
|
+
"""Returns the JSON Schema for the manifest model.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A dictionary containing the JSON Schema with $schema field set.
|
|
18
|
+
"""
|
|
19
|
+
schema = Manifest.model_json_schema(by_alias=True)
|
|
20
|
+
schema["$schema"] = JSON_SCHEMA_URL
|
|
21
|
+
return schema
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def manifest_schema_text() -> str:
|
|
25
|
+
"""Returns the JSON Schema as pretty-printed JSON string.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A formatted JSON string representation of the schema.
|
|
29
|
+
"""
|
|
30
|
+
return json.dumps(manifest_schema(), indent=2, ensure_ascii=True)
|
pcf_toolkit/models.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Pydantic models for PCF manifest schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from pcf_toolkit.types import (
|
|
10
|
+
UNSUPPORTED_TYPE_VALUES,
|
|
11
|
+
ControlType,
|
|
12
|
+
DependencyLoadType,
|
|
13
|
+
DependencyType,
|
|
14
|
+
PlatformActionType,
|
|
15
|
+
PlatformLibraryName,
|
|
16
|
+
PropertySetUsage,
|
|
17
|
+
PropertyUsage,
|
|
18
|
+
RequiredFor,
|
|
19
|
+
TypeValue,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
NonEmptyStr = Annotated[str, Field(min_length=1)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PcfBaseModel(BaseModel):
|
|
26
|
+
"""Base model with strict validation for PCF schema."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EnumValue(PcfBaseModel):
|
|
32
|
+
"""Represents a value element for Enum types."""
|
|
33
|
+
|
|
34
|
+
name: NonEmptyStr = Field(description="Name of the enum value.")
|
|
35
|
+
display_name_key: NonEmptyStr = Field(
|
|
36
|
+
alias="display-name-key",
|
|
37
|
+
description="Localized display name for the enum value.",
|
|
38
|
+
)
|
|
39
|
+
value: int = Field(description="Numeric value for the enum option.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TypeElement(PcfBaseModel):
|
|
43
|
+
"""Represents a type element inside types or type-group."""
|
|
44
|
+
|
|
45
|
+
value: TypeValue = Field(description="Data type value for the type element.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TypesElement(PcfBaseModel):
|
|
49
|
+
"""Represents a types element containing type children."""
|
|
50
|
+
|
|
51
|
+
types: list[TypeElement] = Field(
|
|
52
|
+
default_factory=list,
|
|
53
|
+
alias="type",
|
|
54
|
+
description="List of type elements.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="after")
|
|
58
|
+
def _ensure_not_empty(self) -> TypesElement:
|
|
59
|
+
if not self.types:
|
|
60
|
+
raise ValueError("types must include at least one type")
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TypeGroup(PcfBaseModel):
|
|
65
|
+
"""Defines a type-group element."""
|
|
66
|
+
|
|
67
|
+
name: NonEmptyStr = Field(description="Name of the data type group.")
|
|
68
|
+
types: list[TypeElement] = Field(
|
|
69
|
+
default_factory=list,
|
|
70
|
+
alias="type",
|
|
71
|
+
description="Types belonging to the type-group.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@model_validator(mode="after")
|
|
75
|
+
def _ensure_types(self) -> TypeGroup:
|
|
76
|
+
if not self.types:
|
|
77
|
+
raise ValueError("type-group must include at least one type")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Code(PcfBaseModel):
|
|
82
|
+
"""Represents a code element."""
|
|
83
|
+
|
|
84
|
+
path: NonEmptyStr = Field(description="Place where the resource files are located.")
|
|
85
|
+
order: PositiveInt = Field(description="The order in which the resource files should load.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Css(PcfBaseModel):
|
|
89
|
+
"""Represents a css element."""
|
|
90
|
+
|
|
91
|
+
path: NonEmptyStr = Field(description="Relative path where CSS files are located.")
|
|
92
|
+
order: PositiveInt | None = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description="The order in which the CSS files should load.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Img(PcfBaseModel):
|
|
99
|
+
"""Represents an img element."""
|
|
100
|
+
|
|
101
|
+
path: NonEmptyStr = Field(description="Relative path where image files are located.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Resx(PcfBaseModel):
|
|
105
|
+
"""Represents a resx element."""
|
|
106
|
+
|
|
107
|
+
path: NonEmptyStr = Field(description="Relative path where resx files are located.")
|
|
108
|
+
version: NonEmptyStr = Field(description="The current version of the resx file.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PlatformLibrary(PcfBaseModel):
|
|
112
|
+
"""Represents a platform-library element."""
|
|
113
|
+
|
|
114
|
+
name: PlatformLibraryName = Field(description="Either React or Fluent.")
|
|
115
|
+
version: NonEmptyStr = Field(description="The current version of the platform library.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Dependency(PcfBaseModel):
|
|
119
|
+
"""Represents a dependency element."""
|
|
120
|
+
|
|
121
|
+
type: DependencyType = Field(description="Set to control.")
|
|
122
|
+
name: NonEmptyStr = Field(description="Schema name of the library component.")
|
|
123
|
+
order: PositiveInt | None = Field(default=None, description="The order in which the dependent library should load.")
|
|
124
|
+
load_type: DependencyLoadType | None = Field(default=None, alias="load-type", description="Set to onDemand.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Resources(PcfBaseModel):
|
|
128
|
+
"""Represents a resources element."""
|
|
129
|
+
|
|
130
|
+
code: Code = Field(description="Code resource definition.")
|
|
131
|
+
css: list[Css] = Field(default_factory=list, description="CSS resources.")
|
|
132
|
+
img: list[Img] = Field(default_factory=list, description="Image resources.")
|
|
133
|
+
resx: list[Resx] = Field(default_factory=list, description="Resx resources.")
|
|
134
|
+
platform_library: list[PlatformLibrary] = Field(
|
|
135
|
+
default_factory=list, alias="platform-library", description="Platform library resources."
|
|
136
|
+
)
|
|
137
|
+
dependency: list[Dependency] = Field(default_factory=list, description="Dependent library resources.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Domain(PcfBaseModel):
|
|
141
|
+
"""Represents a domain element within external-service-usage."""
|
|
142
|
+
|
|
143
|
+
value: NonEmptyStr = Field(description="Domain name used by the component.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ExternalServiceUsage(PcfBaseModel):
|
|
147
|
+
"""Represents external-service-usage element."""
|
|
148
|
+
|
|
149
|
+
enabled: bool | None = Field(
|
|
150
|
+
default=None,
|
|
151
|
+
description=("Indicates whether this control uses an external service."),
|
|
152
|
+
)
|
|
153
|
+
domain: list[Domain] = Field(default_factory=list, description="Domains referenced by the control.")
|
|
154
|
+
|
|
155
|
+
@model_validator(mode="after")
|
|
156
|
+
def _enabled_requires_domains(self) -> ExternalServiceUsage:
|
|
157
|
+
if self.enabled and not self.domain:
|
|
158
|
+
raise ValueError("enabled external-service-usage requires at least one domain")
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PlatformAction(PcfBaseModel):
|
|
163
|
+
"""Represents a platform-action element."""
|
|
164
|
+
|
|
165
|
+
action_type: PlatformActionType | None = Field(
|
|
166
|
+
default=None,
|
|
167
|
+
alias="action-type",
|
|
168
|
+
description="Set to afterPageLoad.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class PropertyDependency(PcfBaseModel):
|
|
173
|
+
"""Represents a property-dependency element."""
|
|
174
|
+
|
|
175
|
+
input: NonEmptyStr = Field(description="The name of the input property.")
|
|
176
|
+
output: NonEmptyStr = Field(description="The name of the output property.")
|
|
177
|
+
required_for: RequiredFor = Field(
|
|
178
|
+
alias="required-for",
|
|
179
|
+
description="Currently supported value is schema.",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PropertyDependencies(PcfBaseModel):
|
|
184
|
+
"""Represents property-dependencies element."""
|
|
185
|
+
|
|
186
|
+
property_dependency: list[PropertyDependency] = Field(
|
|
187
|
+
default_factory=list, alias="property-dependency", description="Property dependency definitions."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@model_validator(mode="after")
|
|
191
|
+
def _ensure_dependencies(self) -> PropertyDependencies:
|
|
192
|
+
if not self.property_dependency:
|
|
193
|
+
raise ValueError("property-dependencies must include at least one property-dependency")
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class UsesFeature(PcfBaseModel):
|
|
198
|
+
"""Represents a uses-feature element."""
|
|
199
|
+
|
|
200
|
+
name: NonEmptyStr = Field(description="Name of the feature declared by the component.")
|
|
201
|
+
required: bool = Field(description="Indicates if the component requires the feature.")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class FeatureUsage(PcfBaseModel):
|
|
205
|
+
"""Represents a feature-usage element."""
|
|
206
|
+
|
|
207
|
+
uses_feature: list[UsesFeature] = Field(
|
|
208
|
+
default_factory=list, alias="uses-feature", description="Features used by the component."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@model_validator(mode="after")
|
|
212
|
+
def _ensure_uses_feature(self) -> FeatureUsage:
|
|
213
|
+
if not self.uses_feature:
|
|
214
|
+
raise ValueError("feature-usage must include at least one uses-feature")
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Event(PcfBaseModel):
|
|
219
|
+
"""Represents an event element."""
|
|
220
|
+
|
|
221
|
+
name: NonEmptyStr = Field(description="Name of the event.")
|
|
222
|
+
display_name_key: NonEmptyStr | None = Field(
|
|
223
|
+
default=None, alias="display-name-key", description="Localized display name for the event."
|
|
224
|
+
)
|
|
225
|
+
description_key: NonEmptyStr | None = Field(
|
|
226
|
+
default=None, alias="description-key", description="Localized description for the event."
|
|
227
|
+
)
|
|
228
|
+
pfx_default_value: NonEmptyStr | None = Field(
|
|
229
|
+
default=None, alias="pfx-default-value", description="Default Power Fx expression for the event."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Property(PcfBaseModel):
|
|
234
|
+
"""Represents a property element."""
|
|
235
|
+
|
|
236
|
+
name: NonEmptyStr = Field(description="Name of the property.")
|
|
237
|
+
display_name_key: NonEmptyStr | None = Field(
|
|
238
|
+
default=None, alias="display-name-key", description="Localized display name for the property."
|
|
239
|
+
)
|
|
240
|
+
description_key: NonEmptyStr | None = Field(
|
|
241
|
+
default=None, alias="description-key", description="Localized description for the property."
|
|
242
|
+
)
|
|
243
|
+
of_type: TypeValue | None = Field(
|
|
244
|
+
default=None, alias="of-type", description="Defines the data type of the property."
|
|
245
|
+
)
|
|
246
|
+
of_type_group: NonEmptyStr | None = Field(
|
|
247
|
+
default=None, alias="of-type-group", description="Name of the type-group as defined in manifest."
|
|
248
|
+
)
|
|
249
|
+
usage: PropertyUsage | None = Field(default=None, description="Identifies the usage of the property.")
|
|
250
|
+
required: bool | None = Field(default=None, description="Whether the property is required or not.")
|
|
251
|
+
default_value: NonEmptyStr | None = Field(
|
|
252
|
+
default=None, alias="default-value", description="Default configuration value provided to the component."
|
|
253
|
+
)
|
|
254
|
+
pfx_default_value: NonEmptyStr | None = Field(
|
|
255
|
+
default=None,
|
|
256
|
+
alias="pfx-default-value",
|
|
257
|
+
description="Default Power Fx expression value provided to the component.",
|
|
258
|
+
)
|
|
259
|
+
types: TypesElement | None = Field(default=None, description="Types element for this property.")
|
|
260
|
+
values: list[EnumValue] = Field(
|
|
261
|
+
default_factory=list, alias="value", description="Enum values when of-type is Enum."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
@field_validator("of_type")
|
|
265
|
+
@classmethod
|
|
266
|
+
def _validate_supported_type(cls, value: TypeValue | None) -> TypeValue | None:
|
|
267
|
+
if value is None:
|
|
268
|
+
return value
|
|
269
|
+
if value.value in UNSUPPORTED_TYPE_VALUES:
|
|
270
|
+
raise ValueError(f"Unsupported of-type value: {value.value}")
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
@model_validator(mode="after")
|
|
274
|
+
def _validate_type_requirements(self) -> Property:
|
|
275
|
+
if not self.of_type and not self.of_type_group:
|
|
276
|
+
raise ValueError("property requires of-type or of-type-group")
|
|
277
|
+
if self.of_type == TypeValue.ENUM and not self.values:
|
|
278
|
+
raise ValueError("Enum properties require at least one value element")
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class PropertySet(PcfBaseModel):
|
|
283
|
+
"""Represents a property-set element within a data-set."""
|
|
284
|
+
|
|
285
|
+
name: NonEmptyStr = Field(description="Name of the column.")
|
|
286
|
+
display_name_key: NonEmptyStr = Field(
|
|
287
|
+
alias="display-name-key", description="Localized display name for the property set."
|
|
288
|
+
)
|
|
289
|
+
description_key: NonEmptyStr | None = Field(
|
|
290
|
+
default=None, alias="description-key", description="Localized description for the property set."
|
|
291
|
+
)
|
|
292
|
+
of_type: TypeValue | None = Field(
|
|
293
|
+
default=None, alias="of-type", description="Defines the data type of the property set."
|
|
294
|
+
)
|
|
295
|
+
of_type_group: NonEmptyStr | None = Field(
|
|
296
|
+
default=None, alias="of-type-group", description="Name of the type-group as defined in manifest."
|
|
297
|
+
)
|
|
298
|
+
usage: PropertySetUsage | None = Field(default=None, description="Usage value for the property set.")
|
|
299
|
+
required: bool | None = Field(default=None, description="Indicates whether the property is required.")
|
|
300
|
+
types: TypesElement | None = Field(default=None, description="Types element for this property set.")
|
|
301
|
+
|
|
302
|
+
@field_validator("of_type")
|
|
303
|
+
@classmethod
|
|
304
|
+
def _validate_supported_type(cls, value: TypeValue | None) -> TypeValue | None:
|
|
305
|
+
if value is None:
|
|
306
|
+
return value
|
|
307
|
+
if value.value in UNSUPPORTED_TYPE_VALUES:
|
|
308
|
+
raise ValueError(f"Unsupported of-type value: {value.value}")
|
|
309
|
+
return value
|
|
310
|
+
|
|
311
|
+
@model_validator(mode="after")
|
|
312
|
+
def _validate_type_requirements(self) -> PropertySet:
|
|
313
|
+
if not self.of_type and not self.of_type_group:
|
|
314
|
+
raise ValueError("property-set requires of-type or of-type-group")
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class DataSet(PcfBaseModel):
|
|
319
|
+
"""Represents a data-set element."""
|
|
320
|
+
|
|
321
|
+
name: NonEmptyStr = Field(description="Name of the grid.")
|
|
322
|
+
display_name_key: NonEmptyStr = Field(alias="display-name-key", description="Defines the name of the property.")
|
|
323
|
+
description_key: NonEmptyStr | None = Field(
|
|
324
|
+
default=None, alias="description-key", description="Defines the description of the property."
|
|
325
|
+
)
|
|
326
|
+
cds_data_set_options: NonEmptyStr | None = Field(
|
|
327
|
+
default=None,
|
|
328
|
+
alias="cds-data-set-options",
|
|
329
|
+
description=("Displays the Commandbar, ViewSelector, QuickFind if set to true."),
|
|
330
|
+
)
|
|
331
|
+
property_set: list[PropertySet] = Field(
|
|
332
|
+
default_factory=list, alias="property-set", description="Property sets defined within the dataset."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Control(PcfBaseModel):
|
|
337
|
+
"""Represents a control element."""
|
|
338
|
+
|
|
339
|
+
namespace: NonEmptyStr = Field(description="Defines the object prototype of the component.")
|
|
340
|
+
constructor: NonEmptyStr = Field(description="A method for initializing the object.")
|
|
341
|
+
version: NonEmptyStr = Field(description="Semantic version of the component.")
|
|
342
|
+
display_name_key: NonEmptyStr = Field(
|
|
343
|
+
alias="display-name-key", description="Defines the name of the control visible in the UI."
|
|
344
|
+
)
|
|
345
|
+
description_key: NonEmptyStr | None = Field(
|
|
346
|
+
default=None, alias="description-key", description="Defines the description of the component visible in the UI."
|
|
347
|
+
)
|
|
348
|
+
control_type: ControlType | None = Field(
|
|
349
|
+
default=None, alias="control-type", description="Defines whether the control is standard or virtual."
|
|
350
|
+
)
|
|
351
|
+
preview_image: NonEmptyStr | None = Field(
|
|
352
|
+
default=None, alias="preview-image", description="Image used on customization screens."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
property: list[Property] = Field(default_factory=list, description="Property definitions.")
|
|
356
|
+
event: list[Event] = Field(default_factory=list, description="Event definitions.")
|
|
357
|
+
data_set: list[DataSet] = Field(default_factory=list, alias="data-set", description="Data set definitions.")
|
|
358
|
+
type_group: list[TypeGroup] = Field(default_factory=list, alias="type-group", description="Type group definitions.")
|
|
359
|
+
property_dependencies: PropertyDependencies | None = Field(
|
|
360
|
+
default=None, alias="property-dependencies", description="Property dependency definitions."
|
|
361
|
+
)
|
|
362
|
+
feature_usage: FeatureUsage | None = Field(
|
|
363
|
+
default=None, alias="feature-usage", description="Feature usage definitions."
|
|
364
|
+
)
|
|
365
|
+
external_service_usage: ExternalServiceUsage | None = Field(
|
|
366
|
+
default=None, alias="external-service-usage", description="External service usage definition."
|
|
367
|
+
)
|
|
368
|
+
platform_action: PlatformAction | None = Field(
|
|
369
|
+
default=None, alias="platform-action", description="Platform action configuration."
|
|
370
|
+
)
|
|
371
|
+
resources: Resources = Field(description="Resource definitions.")
|
|
372
|
+
|
|
373
|
+
@field_validator("namespace", "constructor")
|
|
374
|
+
@classmethod
|
|
375
|
+
def _validate_alpha_num(cls, value: str) -> str:
|
|
376
|
+
if not value.isalnum():
|
|
377
|
+
raise ValueError("Value must contain only letters or numbers")
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class Manifest(PcfBaseModel):
|
|
382
|
+
"""Represents the manifest root element."""
|
|
383
|
+
|
|
384
|
+
control: Control = Field(description="Control definition.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Proxy helpers for PCF Toolkit."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled mitmproxy addons."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Mitmproxy addon that redirects PCF webresource requests to localhost."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from mitmproxy import http
|
|
9
|
+
|
|
10
|
+
PCF_NAME = os.getenv("PCF_COMPONENT_NAME")
|
|
11
|
+
PCF_EXPECTED_PATH = os.getenv("PCF_EXPECTED_PATH", "/webresources/{PCF_NAME}/")
|
|
12
|
+
HTTP_SERVER_PORT = int(os.getenv("HTTP_SERVER_PORT", "8082"))
|
|
13
|
+
HTTP_SERVER_HOST = os.getenv("HTTP_SERVER_HOST", "localhost")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _matches_expected_path(request_url: str, request_path: str) -> tuple[bool, str]:
|
|
17
|
+
"""Checks if request matches expected PCF webresource path.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
request_url: Full request URL.
|
|
21
|
+
request_path: Request path component.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple of (matches, remainder) where remainder is the path after the
|
|
25
|
+
expected base.
|
|
26
|
+
"""
|
|
27
|
+
if not PCF_NAME:
|
|
28
|
+
return False, ""
|
|
29
|
+
expected_base = PCF_EXPECTED_PATH.replace("{PCF_NAME}", PCF_NAME)
|
|
30
|
+
if expected_base in request_path:
|
|
31
|
+
target = request_path
|
|
32
|
+
elif expected_base in request_url:
|
|
33
|
+
target = request_url
|
|
34
|
+
else:
|
|
35
|
+
return False, ""
|
|
36
|
+
pattern = re.escape(expected_base) + r"(.*)"
|
|
37
|
+
match = re.search(pattern, target)
|
|
38
|
+
if not match:
|
|
39
|
+
return False, ""
|
|
40
|
+
return True, match.group(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def request(flow: http.HTTPFlow) -> None:
|
|
44
|
+
"""Mitmproxy request hook that redirects PCF webresource requests.
|
|
45
|
+
|
|
46
|
+
Intercepts requests matching the expected PCF webresource path and redirects
|
|
47
|
+
them to the local HTTP server.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
flow: HTTP flow object from mitmproxy.
|
|
51
|
+
"""
|
|
52
|
+
if not PCF_NAME:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
matches, remainder = _matches_expected_path(flow.request.url, flow.request.path)
|
|
56
|
+
if not matches:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
dynamic_path = remainder.lstrip("/")
|
|
60
|
+
if dynamic_path:
|
|
61
|
+
redirect_path = f"/{dynamic_path}"
|
|
62
|
+
else:
|
|
63
|
+
redirect_path = "/"
|
|
64
|
+
|
|
65
|
+
flow.request.host = HTTP_SERVER_HOST
|
|
66
|
+
flow.request.port = HTTP_SERVER_PORT
|
|
67
|
+
flow.request.scheme = "http"
|
|
68
|
+
flow.request.path = redirect_path
|
|
69
|
+
flow.request.headers["if-none-match"] = ""
|
|
70
|
+
flow.request.headers["cache-control"] = "no-cache"
|