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/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"