syntara-plugin-sdk 0.1.0a1__tar.gz

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 (30) hide show
  1. syntara_plugin_sdk-0.1.0a1/.gitignore +66 -0
  2. syntara_plugin_sdk-0.1.0a1/PKG-INFO +30 -0
  3. syntara_plugin_sdk-0.1.0a1/README.md +7 -0
  4. syntara_plugin_sdk-0.1.0a1/pyproject.toml +49 -0
  5. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/__init__.py +109 -0
  6. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/artifact.py +243 -0
  7. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/artifact_archive.py +356 -0
  8. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/authoring.py +1525 -0
  9. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/catalog.py +364 -0
  10. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/cli.py +1672 -0
  11. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/oci.py +64 -0
  12. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/py.typed +1 -0
  13. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/registry.py +638 -0
  14. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/settings.py +360 -0
  15. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/testgen.py +420 -0
  16. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/sdk/workload.py +142 -0
  17. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/__init__.py +99 -0
  18. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/bindings_factory.py +385 -0
  19. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/chaining.py +170 -0
  20. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/conformance.py +168 -0
  21. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/context_builder.py +119 -0
  22. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/equivalence.py +119 -0
  23. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/errors.py +7 -0
  24. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/invocation.py +405 -0
  25. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/loader.py +85 -0
  26. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/manifests.py +170 -0
  27. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/payloads.py +148 -0
  28. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/plugin.py +53 -0
  29. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/py.typed +0 -0
  30. syntara_plugin_sdk-0.1.0a1/src/syntara_plugin/testing/redaction.py +137 -0
@@ -0,0 +1,66 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ venv/
28
+ ENV/
29
+ env/
30
+ .venv
31
+
32
+ # IDEs
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+ .DS_Store
39
+
40
+ # Testing
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+
47
+ # uv
48
+ .uv/
49
+
50
+ # Step SDK specific
51
+ *.compiled.json
52
+ step-definition.json
53
+ examples/*/step-definition.json
54
+
55
+ # Databases
56
+ *.db
57
+ *.sqlite
58
+ *.sqlite3
59
+
60
+ # Logs
61
+ *.log
62
+
63
+ # Environment variables
64
+ .env
65
+ .env.local
66
+ coverage.xml
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.5
2
+ Name: syntara-plugin-sdk
3
+ Version: 0.1.0a1
4
+ Summary: Authoring SDK for Syntara plugins, including the local test harness
5
+ Project-URL: Homepage, https://github.com/syntara-orchestration/syntara-plugin-sdk
6
+ Project-URL: Repository, https://github.com/syntara-orchestration/syntara-plugin-sdk
7
+ Project-URL: Issues, https://github.com/syntara-orchestration/syntara-plugin-sdk/issues
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: syntara-plugin-contracts==0.1.0a1
20
+ Provides-Extra: testing
21
+ Requires-Dist: pytest>=8.0; extra == 'testing'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Syntara Plugin SDK
25
+
26
+ `syntara-plugin-sdk` installs the `syntara-plugin` command-line interface for
27
+ creating, validating, packaging, inspecting, and publishing Syntara plugins.
28
+
29
+ See the [Syntara Plugin SDK repository](https://github.com/syntara-orchestration/syntara-plugin-sdk)
30
+ for installation, authoring guides, and the normative plugin contracts.
@@ -0,0 +1,7 @@
1
+ # Syntara Plugin SDK
2
+
3
+ `syntara-plugin-sdk` installs the `syntara-plugin` command-line interface for
4
+ creating, validating, packaging, inspecting, and publishing Syntara plugins.
5
+
6
+ See the [Syntara Plugin SDK repository](https://github.com/syntara-orchestration/syntara-plugin-sdk)
7
+ for installation, authoring guides, and the normative plugin contracts.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "syntara-plugin-sdk"
3
+ version = "0.1.0a1"
4
+ description = "Authoring SDK for Syntara plugins, including the local test harness"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "PyYAML>=6.0",
10
+ "syntara-plugin-contracts==0.1.0a1",
11
+ ]
12
+ classifiers = [
13
+ "Development Status :: 2 - Pre-Alpha",
14
+ "License :: OSI Approved :: Apache Software License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/syntara-orchestration/syntara-plugin-sdk"
25
+ Repository = "https://github.com/syntara-orchestration/syntara-plugin-sdk"
26
+ Issues = "https://github.com/syntara-orchestration/syntara-plugin-sdk/issues"
27
+
28
+ [project.optional-dependencies]
29
+ testing = [
30
+ "pytest>=8.0",
31
+ ]
32
+
33
+ [project.entry-points.pytest11]
34
+ syntara_plugin_testing = "syntara_plugin.testing.plugin"
35
+
36
+ [tool.uv.sources]
37
+ syntara-plugin-contracts = { workspace = true }
38
+
39
+ [project.scripts]
40
+ syntara-plugin = "syntara_plugin.sdk.cli:main"
41
+
42
+ [build-system]
43
+ # Keep release builds reproducible; upgrades are reviewed like any other
44
+ # build-tool supply-chain change.
45
+ requires = ["hatchling==1.32.4"]
46
+ build-backend = "hatchling.build"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/syntara_plugin"]
@@ -0,0 +1,109 @@
1
+ """Authoring SDK package boundary for Syntara plugins."""
2
+
3
+ from syntara_plugin.contracts import ContractBundle, load_bundle
4
+
5
+ from .artifact import (
6
+ OCI_ARTIFACT_TYPE,
7
+ OCI_CONFIG_MEDIA_TYPE,
8
+ OCI_CONTENT_BUNDLE_MEDIA_TYPE,
9
+ OCI_PLUGIN_MANIFEST_MEDIA_TYPE,
10
+ ArtifactBuildError,
11
+ PluginArtifact,
12
+ build_plugin_artifact,
13
+ )
14
+ from .artifact_archive import (
15
+ OCI_LAYOUT_VERSION,
16
+ ArtifactArchiveError,
17
+ read_plugin_artifact_archive,
18
+ write_plugin_artifact_archive,
19
+ )
20
+ from .authoring import (
21
+ BuildRequest,
22
+ CompilationError,
23
+ CompilationResult,
24
+ CompiledAsset,
25
+ Diagnostic,
26
+ Plugin,
27
+ SourceLocation,
28
+ Target,
29
+ compile_model,
30
+ compile_workspace,
31
+ )
32
+ from .catalog import (
33
+ OCI_CATALOG_INDEX_ARTIFACT_TYPE,
34
+ OCI_CATALOG_INDEX_CONFIG_MEDIA_TYPE,
35
+ OCI_CATALOG_INDEX_CONTENT_MEDIA_TYPE,
36
+ CatalogIndexArtifact,
37
+ CatalogIndexArtifactBuildError,
38
+ CatalogIndexUpdateError,
39
+ build_catalog_index_artifact,
40
+ merge_catalog_entry,
41
+ recover_catalog_entry,
42
+ )
43
+ from .oci import OCI_IMAGE_MANIFEST_MEDIA_TYPE, OciBlob, OciDescriptor
44
+ from .registry import (
45
+ CatalogIndexPublication,
46
+ CatalogIndexPublicationError,
47
+ CatalogIndexPublicationTarget,
48
+ CatalogIndexPublisher,
49
+ CatalogIndexSnapshot,
50
+ OciRegistryResponse,
51
+ OciRegistryTransport,
52
+ PluginArtifactPublication,
53
+ PluginArtifactPublicationError,
54
+ PluginArtifactPublicationTarget,
55
+ PluginArtifactPublisher,
56
+ RegistryCredentials,
57
+ UrllibOciRegistryTransport,
58
+ )
59
+
60
+ __all__ = [
61
+ "ArtifactBuildError",
62
+ "ArtifactArchiveError",
63
+ "BuildRequest",
64
+ "CatalogIndexArtifact",
65
+ "CatalogIndexArtifactBuildError",
66
+ "CatalogIndexUpdateError",
67
+ "CatalogIndexPublication",
68
+ "CatalogIndexPublicationError",
69
+ "CatalogIndexPublicationTarget",
70
+ "CatalogIndexPublisher",
71
+ "CatalogIndexSnapshot",
72
+ "CompiledAsset",
73
+ "CompilationError",
74
+ "CompilationResult",
75
+ "ContractBundle",
76
+ "Diagnostic",
77
+ "OCI_ARTIFACT_TYPE",
78
+ "OCI_CATALOG_INDEX_ARTIFACT_TYPE",
79
+ "OCI_CATALOG_INDEX_CONFIG_MEDIA_TYPE",
80
+ "OCI_CATALOG_INDEX_CONTENT_MEDIA_TYPE",
81
+ "OCI_CONFIG_MEDIA_TYPE",
82
+ "OCI_CONTENT_BUNDLE_MEDIA_TYPE",
83
+ "OCI_IMAGE_MANIFEST_MEDIA_TYPE",
84
+ "OCI_LAYOUT_VERSION",
85
+ "OCI_PLUGIN_MANIFEST_MEDIA_TYPE",
86
+ "OciBlob",
87
+ "OciDescriptor",
88
+ "OciRegistryResponse",
89
+ "OciRegistryTransport",
90
+ "Plugin",
91
+ "PluginArtifact",
92
+ "PluginArtifactPublication",
93
+ "PluginArtifactPublicationError",
94
+ "PluginArtifactPublicationTarget",
95
+ "PluginArtifactPublisher",
96
+ "RegistryCredentials",
97
+ "SourceLocation",
98
+ "Target",
99
+ "UrllibOciRegistryTransport",
100
+ "build_plugin_artifact",
101
+ "build_catalog_index_artifact",
102
+ "merge_catalog_entry",
103
+ "recover_catalog_entry",
104
+ "compile_model",
105
+ "compile_workspace",
106
+ "load_bundle",
107
+ "read_plugin_artifact_archive",
108
+ "write_plugin_artifact_archive",
109
+ ]
@@ -0,0 +1,243 @@
1
+ """Deterministic, offline OCI artifact assembly for compiled Syntara plugins.
2
+
3
+ This module deliberately stops before transport and trust operations. It turns
4
+ an already-validated :class:`CompilationResult` into content-addressed OCI
5
+ blobs; registry upload, signing, and policy verification remain separate
6
+ adapters owned by later increments.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import gzip
12
+ import tarfile
13
+ import unicodedata
14
+ from collections.abc import Mapping
15
+ from dataclasses import dataclass
16
+ from hashlib import sha256
17
+ from io import BytesIO
18
+ from pathlib import PurePosixPath
19
+
20
+ from .authoring import CompilationResult, CompiledAsset
21
+ from .oci import OCI_IMAGE_MANIFEST_MEDIA_TYPE, OciBlob, canonical_json_bytes
22
+
23
+ OCI_ARTIFACT_TYPE = "application/vnd.syntara.plugin.v1"
24
+ OCI_CONFIG_MEDIA_TYPE = "application/vnd.syntara.plugin.config.v1+json"
25
+ OCI_PLUGIN_MANIFEST_MEDIA_TYPE = "application/vnd.syntara.plugin.manifest.v1+yaml"
26
+ OCI_CONTENT_BUNDLE_MEDIA_TYPE = "application/vnd.syntara.plugin.content.v1.tar+gzip"
27
+
28
+ _SCHEMA_SOURCE_FIELDS = frozenset({"configuration", "credentialSchema", "error", "input", "output"})
29
+ _HTTP_MAPPING_SOURCE_FIELDS = frozenset(
30
+ {"runtime/operation/requestMap", "runtime/operation/responseMap"}
31
+ )
32
+ _OCI_IMAGE_TITLE_ANNOTATION = "org.opencontainers.image.title"
33
+ _DOCUMENTATION_REFERENCE = "plugin.yaml#/spec/documentation"
34
+
35
+
36
+ class ArtifactBuildError(ValueError):
37
+ """Raised when an in-process compilation result cannot form a safe artifact."""
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class PluginArtifact:
42
+ """Offline OCI artifact ready for a later signing or registry adapter."""
43
+
44
+ config: OciBlob
45
+ plugin_manifest: OciBlob
46
+ content_bundle: OciBlob
47
+ manifest: OciBlob
48
+
49
+ @property
50
+ def digest(self) -> str:
51
+ """Return the immutable OCI manifest digest used by catalogs/installers."""
52
+
53
+ return self.manifest.descriptor.digest
54
+
55
+ @property
56
+ def blobs(self) -> Mapping[str, OciBlob]:
57
+ """Return every uploadable blob indexed by its verified digest."""
58
+
59
+ return {
60
+ blob.descriptor.digest: blob
61
+ for blob in (self.config, self.plugin_manifest, self.content_bundle, self.manifest)
62
+ }
63
+
64
+
65
+ def build_plugin_artifact(compilation: CompilationResult) -> PluginArtifact:
66
+ """Build a deterministic metadata-only OCI artifact from compiler output.
67
+
68
+ The plugin metadata is canonical JSON, which is valid YAML 1.2 and can
69
+ therefore safely use the versioned YAML media type. The content bundle
70
+ preserves exact source bytes for every externalized asset so consumers can
71
+ verify descriptor, config, and archive inventories independently.
72
+ """
73
+
74
+ descriptor = compilation.descriptor
75
+ metadata = descriptor.get("metadata") if isinstance(descriptor, Mapping) else None
76
+ if descriptor.get("kind") != "PluginDescriptor" or not isinstance(metadata, Mapping):
77
+ raise ArtifactBuildError(
78
+ "Compilation result must contain a PluginDescriptor metadata mapping."
79
+ )
80
+ namespace = metadata.get("namespace")
81
+ name = metadata.get("name")
82
+ version = metadata.get("version")
83
+ if not all(isinstance(value, str) and value for value in (namespace, name, version)):
84
+ raise ArtifactBuildError(
85
+ "PluginDescriptor metadata must include namespace, name, and version."
86
+ )
87
+
88
+ _validate_assets(compilation.assets)
89
+ plugin_manifest = OciBlob.create(
90
+ compilation.canonical_json(),
91
+ OCI_PLUGIN_MANIFEST_MEDIA_TYPE,
92
+ annotations={_OCI_IMAGE_TITLE_ANNOTATION: "plugin.manifest.yaml"},
93
+ )
94
+ content_bundle = OciBlob.create(
95
+ _build_content_bundle(compilation.assets),
96
+ OCI_CONTENT_BUNDLE_MEDIA_TYPE,
97
+ annotations={_OCI_IMAGE_TITLE_ANNOTATION: "content.tar.gz"},
98
+ )
99
+ config = OciBlob.create(
100
+ canonical_json_bytes(
101
+ {
102
+ "apiVersion": "syntara.plugin.config/v1alpha1",
103
+ "kind": "PluginArtifactConfig",
104
+ "pluginDescriptorDigest": compilation.digest,
105
+ "assetIndex": compilation.asset_index,
106
+ }
107
+ ),
108
+ OCI_CONFIG_MEDIA_TYPE,
109
+ )
110
+ manifest = OciBlob.create(
111
+ canonical_json_bytes(
112
+ {
113
+ "schemaVersion": 2,
114
+ "mediaType": OCI_IMAGE_MANIFEST_MEDIA_TYPE,
115
+ "artifactType": OCI_ARTIFACT_TYPE,
116
+ "config": config.descriptor.as_dict(),
117
+ "layers": [
118
+ plugin_manifest.descriptor.as_dict(),
119
+ content_bundle.descriptor.as_dict(),
120
+ ],
121
+ "annotations": {
122
+ _OCI_IMAGE_TITLE_ANNOTATION: f"{namespace}/{name}",
123
+ "org.opencontainers.image.version": version,
124
+ },
125
+ }
126
+ ),
127
+ OCI_IMAGE_MANIFEST_MEDIA_TYPE,
128
+ )
129
+ return PluginArtifact(config, plugin_manifest, content_bundle, manifest)
130
+
131
+
132
+ def _validate_assets(assets: Mapping[str, CompiledAsset]) -> None:
133
+ seen_casefold: dict[str, str] = {}
134
+ for path, asset in assets.items():
135
+ _validate_asset(path, asset)
136
+ folded = path.casefold()
137
+ existing = seen_casefold.setdefault(folded, path)
138
+ if existing != path:
139
+ raise ArtifactBuildError(
140
+ f"Asset path {path!r} collides with {existing!r} on case-insensitive filesystems."
141
+ )
142
+
143
+
144
+ def _validate_asset(path: str, asset: CompiledAsset) -> None:
145
+ if path != asset.path:
146
+ raise ArtifactBuildError(
147
+ f"Asset index key {path!r} does not match asset path {asset.path!r}."
148
+ )
149
+ _validate_asset_path(path)
150
+ _validate_asset_content(path, asset)
151
+ _validate_asset_references(path, asset)
152
+
153
+
154
+ def _validate_asset_path(path: str) -> None:
155
+ parts = PurePosixPath(path).parts
156
+ invalid_path = any(
157
+ (
158
+ not path,
159
+ "\\" in path,
160
+ path.startswith("/"),
161
+ path != unicodedata.normalize("NFC", path),
162
+ len(path) > 1024,
163
+ _has_control_character(path),
164
+ _has_unsafe_path_part(parts),
165
+ )
166
+ )
167
+ if invalid_path:
168
+ raise ArtifactBuildError(f"Asset path {path!r} is not a normalized relative POSIX path.")
169
+
170
+
171
+ def _has_control_character(path: str) -> bool:
172
+ return any(ord(character) < 32 or ord(character) == 127 for character in path)
173
+
174
+
175
+ def _has_unsafe_path_part(parts: tuple[str, ...]) -> bool:
176
+ return any(part in {"", ".", ".."} or part.startswith(".") for part in parts)
177
+
178
+
179
+ def _validate_asset_content(path: str, asset: CompiledAsset) -> None:
180
+ if f"sha256:{sha256(asset.content).hexdigest()}" != asset.source_digest:
181
+ raise ArtifactBuildError(f"Asset {path!r} source digest does not match its content.")
182
+
183
+
184
+ def _validate_asset_references(path: str, asset: CompiledAsset) -> None:
185
+ if not asset.references or asset.references != tuple(sorted(set(asset.references))):
186
+ raise ArtifactBuildError(f"Asset {path!r} must have sorted, unique source references.")
187
+ for reference in asset.references:
188
+ _validate_asset_reference(path, asset.media_type, reference)
189
+
190
+
191
+ def _validate_asset_reference(path: str, media_type: str, reference: str) -> None:
192
+ if reference == _DOCUMENTATION_REFERENCE:
193
+ if media_type != "text/markdown" or not path.endswith(".md"):
194
+ raise ArtifactBuildError(
195
+ f"Asset {path!r} has an invalid documentation reference {reference!r}."
196
+ )
197
+ return
198
+ expected_media_type = _reference_media_type(path, reference)
199
+ if media_type != expected_media_type:
200
+ raise ArtifactBuildError(f"Asset {path!r} has an invalid media type for {reference!r}.")
201
+
202
+
203
+ def _reference_media_type(path: str, reference: str) -> str:
204
+ """Return the one allowed media type for a typed asset reference."""
205
+
206
+ source_path, separator, field = reference.partition("#/spec/")
207
+ if not separator or not source_path:
208
+ raise ArtifactBuildError(f"Asset {path!r} has an invalid source reference {reference!r}.")
209
+ if field in _SCHEMA_SOURCE_FIELDS:
210
+ prefix = "application/schema+"
211
+ elif field in _HTTP_MAPPING_SOURCE_FIELDS:
212
+ prefix = "application/"
213
+ else:
214
+ raise ArtifactBuildError(f"Asset {path!r} has an invalid source reference {reference!r}.")
215
+ if path.endswith(".json"):
216
+ return f"{prefix}json"
217
+ if path.endswith((".yaml", ".yml")):
218
+ return f"{prefix}yaml"
219
+ if path.endswith(".md"):
220
+ kind = "schema" if field in _SCHEMA_SOURCE_FIELDS else "HTTP mapping"
221
+ raise ArtifactBuildError(f"Asset {path!r} has a non-{kind} media type for {reference!r}.")
222
+ raise ArtifactBuildError(f"Asset {path!r} must use a JSON, YAML, or Markdown filename.")
223
+
224
+
225
+ def _build_content_bundle(assets: Mapping[str, CompiledAsset]) -> bytes:
226
+ """Create a reproducible gzip-compressed tar without filesystem extraction."""
227
+
228
+ output = BytesIO()
229
+ with gzip.GzipFile(fileobj=output, mode="wb", mtime=0, filename="") as compressed:
230
+ with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive:
231
+ for path in sorted(assets):
232
+ asset = assets[path]
233
+ entry = tarfile.TarInfo(name=path)
234
+ entry.size = len(asset.content)
235
+ entry.mode = 0o644
236
+ entry.uid = 0
237
+ entry.gid = 0
238
+ entry.uname = ""
239
+ entry.gname = ""
240
+ entry.mtime = 0
241
+ entry.pax_headers = {}
242
+ archive.addfile(entry, BytesIO(asset.content))
243
+ return output.getvalue()