lumera 0.26.0__tar.gz → 0.28.0.dev1__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.
- {lumera-0.26.0 → lumera-0.28.0.dev1}/PKG-INFO +2 -2
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/__init__.py +7 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/_utils.py +20 -17
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/flags.py +2 -2
- lumera-0.28.0.dev1/lumera/functions/__init__.py +55 -0
- lumera-0.28.0.dev1/lumera/functions/__main__.py +5 -0
- lumera-0.28.0.dev1/lumera/functions/_bundle.py +194 -0
- lumera-0.28.0.dev1/lumera/functions/_core.py +382 -0
- lumera-0.28.0.dev1/lumera/functions/_runner.py +163 -0
- lumera-0.28.0.dev1/lumera/integrations/netsuite.py +474 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/pb.py +24 -1
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/sdk.py +54 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/PKG-INFO +2 -2
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/SOURCES.txt +7 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/pyproject.toml +2 -2
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_flags.py +1 -1
- lumera-0.28.0.dev1/tests/test_functions.py +528 -0
- lumera-0.28.0.dev1/tests/test_integrations_netsuite.py +330 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_sdk.py +19 -1
- lumera-0.26.0/lumera/integrations/netsuite.py +0 -230
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/_live.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/agents.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/automations.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/email.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/exceptions.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/files.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/google.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/__init__.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/bill.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/google.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/slack.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/llm.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/storage.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/webhooks.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/requires.txt +1 -1
- {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/setup.cfg +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_agents_live.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_automations.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_webhooks.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lumera
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0.dev1
|
|
4
4
|
Summary: SDK for building on Lumera platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: requests
|
|
7
7
|
Requires-Dist: python-dotenv
|
|
8
8
|
Requires-Dist: google-api-python-client==2.173.0
|
|
9
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.3
|
|
9
10
|
Requires-Dist: simple-salesforce==1.12.6
|
|
10
11
|
Requires-Dist: slack_sdk==3.35.0
|
|
11
12
|
Provides-Extra: dev
|
|
@@ -28,7 +29,6 @@ Requires-Dist: openai-agents<1.0.0,>=0.6.5; extra == "full"
|
|
|
28
29
|
Requires-Dist: openpyxl==3.1.5; extra == "full"
|
|
29
30
|
Requires-Dist: pandas==2.3.0; extra == "full"
|
|
30
31
|
Requires-Dist: pdfplumber; extra == "full"
|
|
31
|
-
Requires-Dist: pydantic<3.0.0,>=2.12.3; extra == "full"
|
|
32
32
|
Requires-Dist: PyGithub==2.6.1; extra == "full"
|
|
33
33
|
Requires-Dist: python-dotenv==1.1.0; extra == "full"
|
|
34
34
|
Requires-Dist: pyzmq; extra == "full"
|
|
@@ -18,6 +18,7 @@ from . import (
|
|
|
18
18
|
automations,
|
|
19
19
|
email,
|
|
20
20
|
exceptions,
|
|
21
|
+
functions,
|
|
21
22
|
integrations,
|
|
22
23
|
llm,
|
|
23
24
|
pb,
|
|
@@ -48,6 +49,7 @@ from .exceptions import (
|
|
|
48
49
|
# Import file types for Pydantic automation inputs
|
|
49
50
|
from .files import LumeraFile, LumeraFiles
|
|
50
51
|
from .flags import feature_flag, feature_flags, is_feature_enabled
|
|
52
|
+
from .functions import function
|
|
51
53
|
|
|
52
54
|
# Import key SDK helpers to expose them at the package root.
|
|
53
55
|
from .sdk import (
|
|
@@ -67,6 +69,7 @@ from .sdk import (
|
|
|
67
69
|
replay_hook,
|
|
68
70
|
run_automation,
|
|
69
71
|
save_to_lumera,
|
|
72
|
+
search_records,
|
|
70
73
|
update_automation_run,
|
|
71
74
|
update_collection,
|
|
72
75
|
update_record,
|
|
@@ -88,6 +91,8 @@ __all__ = [
|
|
|
88
91
|
"feature_flag",
|
|
89
92
|
"feature_flags",
|
|
90
93
|
"is_feature_enabled",
|
|
94
|
+
# Callable Function authoring
|
|
95
|
+
"function",
|
|
91
96
|
# Collections (low-level API)
|
|
92
97
|
"list_collections",
|
|
93
98
|
"get_collection",
|
|
@@ -96,6 +101,7 @@ __all__ = [
|
|
|
96
101
|
"delete_collection",
|
|
97
102
|
# Records (low-level API)
|
|
98
103
|
"list_records",
|
|
104
|
+
"search_records",
|
|
99
105
|
"get_record",
|
|
100
106
|
"get_record_by_external_id",
|
|
101
107
|
"create_record",
|
|
@@ -131,6 +137,7 @@ __all__ = [
|
|
|
131
137
|
"storage",
|
|
132
138
|
"llm",
|
|
133
139
|
"exceptions",
|
|
140
|
+
"functions",
|
|
134
141
|
"webhooks",
|
|
135
142
|
"integrations",
|
|
136
143
|
]
|
|
@@ -20,12 +20,18 @@ from dotenv import load_dotenv
|
|
|
20
20
|
|
|
21
21
|
TOKEN_ENV = "LUMERA_TOKEN"
|
|
22
22
|
BASE_URL_ENV = "LUMERA_BASE_URL"
|
|
23
|
+
CREDENTIALS_PATH_ENV = "LUMERA_CREDENTIALS_PATH"
|
|
23
24
|
EXPECTED_TEST_ERROR_ENV = "LUMERA_EXPECTED_TEST_ERROR"
|
|
24
25
|
EXPECTED_TEST_HEADER = "X-Lumera-Expected-Test"
|
|
25
26
|
ENV_PATH = "/root/.env"
|
|
26
27
|
LOCAL_CREDS_PATH = ".lumera/credentials.json"
|
|
27
28
|
GLOBAL_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "lumera", "credentials.json")
|
|
28
29
|
|
|
30
|
+
|
|
31
|
+
def _credentials_paths() -> tuple[str, ...]:
|
|
32
|
+
explicit = os.getenv(CREDENTIALS_PATH_ENV, "").strip()
|
|
33
|
+
return tuple(dict.fromkeys(path for path in (explicit, LOCAL_CREDS_PATH, GLOBAL_CREDS_PATH) if path))
|
|
34
|
+
|
|
29
35
|
load_dotenv(override=False)
|
|
30
36
|
load_dotenv(ENV_PATH, override=False)
|
|
31
37
|
|
|
@@ -37,7 +43,7 @@ def _resolve_api_base() -> str:
|
|
|
37
43
|
|
|
38
44
|
Priority:
|
|
39
45
|
1. LUMERA_BASE_URL environment variable
|
|
40
|
-
2. base_url from credentials file (project-local
|
|
46
|
+
2. base_url from credentials file (explicit runtime, project-local, global)
|
|
41
47
|
3. Default (https://app.lumerahq.com/api)
|
|
42
48
|
"""
|
|
43
49
|
# 1. Environment variable
|
|
@@ -47,7 +53,7 @@ def _resolve_api_base() -> str:
|
|
|
47
53
|
return base if base.endswith("/api") else base + "/api"
|
|
48
54
|
|
|
49
55
|
# 2. Credentials file
|
|
50
|
-
for path in (
|
|
56
|
+
for path in _credentials_paths():
|
|
51
57
|
try:
|
|
52
58
|
if os.path.exists(path):
|
|
53
59
|
with open(path) as f:
|
|
@@ -112,30 +118,27 @@ def get_lumera_token() -> str:
|
|
|
112
118
|
|
|
113
119
|
Priority:
|
|
114
120
|
1. LUMERA_TOKEN environment variable
|
|
115
|
-
2.
|
|
116
|
-
3.
|
|
117
|
-
4. /
|
|
121
|
+
2. LUMERA_CREDENTIALS_PATH (sandbox runtime credentials)
|
|
122
|
+
3. .lumera/credentials.json (project-local, from CLI login --local)
|
|
123
|
+
4. ~/.config/lumera/credentials.json (global, from CLI login)
|
|
124
|
+
5. /root/.env (legacy, for automations in sandbox)
|
|
118
125
|
"""
|
|
119
126
|
# 1. Check environment variable (highest priority)
|
|
120
127
|
token = os.getenv(TOKEN_ENV)
|
|
121
128
|
if token:
|
|
122
129
|
return token
|
|
123
130
|
|
|
124
|
-
# 2. Check project-local credentials
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# 3. Check global credentials
|
|
130
|
-
token = _read_token_from_creds_file(GLOBAL_CREDS_PATH)
|
|
131
|
-
if token:
|
|
132
|
-
return token
|
|
131
|
+
# 2-4. Check explicit runtime, project-local, then global credentials.
|
|
132
|
+
for path in _credentials_paths():
|
|
133
|
+
token = _read_token_from_creds_file(path)
|
|
134
|
+
if token:
|
|
135
|
+
return token
|
|
133
136
|
|
|
134
|
-
#
|
|
137
|
+
# 5. /root/.env is already loaded via dotenv at module load time
|
|
135
138
|
# so if we get here, no token was found anywhere
|
|
136
139
|
raise RuntimeError(
|
|
137
|
-
f"{TOKEN_ENV} not found. Checked: environment, {
|
|
138
|
-
f"{
|
|
140
|
+
f"{TOKEN_ENV} not found. Checked: environment, {_credentials_paths()}, "
|
|
141
|
+
f"{ENV_PATH}. "
|
|
139
142
|
f"Run `lumera login` to authenticate."
|
|
140
143
|
)
|
|
141
144
|
|
|
@@ -4,7 +4,7 @@ Feature flags inside Lumera sandboxes.
|
|
|
4
4
|
The Lumera backend resolves the company's feature flags at sandbox warm and
|
|
5
5
|
writes a typed JSON snapshot to a well-known path (pointed to by the
|
|
6
6
|
``LUMERA_FEATURE_FLAGS_PATH`` env var, default
|
|
7
|
-
``/
|
|
7
|
+
``/lumera_state/feature-flags/flags.json``).
|
|
8
8
|
This module reads that snapshot so sandbox code can gate behavior on a flag.
|
|
9
9
|
|
|
10
10
|
Values are stored in their native type (booleans, numbers, JSON objects), so
|
|
@@ -35,7 +35,7 @@ _logger = logging.getLogger("lumera.sdk")
|
|
|
35
35
|
|
|
36
36
|
# Keep in sync with backend sandboximage.FeatureFlagsPath / FeatureFlagsPathEnv.
|
|
37
37
|
_ENV_PATH = "LUMERA_FEATURE_FLAGS_PATH"
|
|
38
|
-
_DEFAULT_PATH = "/
|
|
38
|
+
_DEFAULT_PATH = "/lumera_state/feature-flags/flags.json"
|
|
39
39
|
|
|
40
40
|
_lock = threading.Lock()
|
|
41
41
|
# Cache entry: (cache_key, flags). cache_key is (path, mtime_ns) so a refreshed
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Define, discover, inspect, and locally invoke typed Lumera Functions."""
|
|
2
|
+
|
|
3
|
+
from ._bundle import (
|
|
4
|
+
FUNCTION_BUNDLE_FORMAT_VERSION,
|
|
5
|
+
FUNCTION_BUNDLE_RUNTIME,
|
|
6
|
+
FunctionBundle,
|
|
7
|
+
build_bundle,
|
|
8
|
+
)
|
|
9
|
+
from ._core import (
|
|
10
|
+
FUNCTION_MANIFEST_SCHEMA_VERSION,
|
|
11
|
+
DuplicateFunctionError,
|
|
12
|
+
FunctionConfirmation,
|
|
13
|
+
FunctionDefinition,
|
|
14
|
+
FunctionDefinitionError,
|
|
15
|
+
FunctionDiscoveryError,
|
|
16
|
+
FunctionEffect,
|
|
17
|
+
FunctionError,
|
|
18
|
+
FunctionExecutionError,
|
|
19
|
+
FunctionMetadata,
|
|
20
|
+
FunctionNotFoundError,
|
|
21
|
+
FunctionOutputSerializationError,
|
|
22
|
+
FunctionValidationError,
|
|
23
|
+
build_manifest,
|
|
24
|
+
discover_modules,
|
|
25
|
+
function,
|
|
26
|
+
get_function_definition,
|
|
27
|
+
invoke_function,
|
|
28
|
+
list_function_definitions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"FUNCTION_BUNDLE_FORMAT_VERSION",
|
|
33
|
+
"FUNCTION_BUNDLE_RUNTIME",
|
|
34
|
+
"FUNCTION_MANIFEST_SCHEMA_VERSION",
|
|
35
|
+
"DuplicateFunctionError",
|
|
36
|
+
"FunctionConfirmation",
|
|
37
|
+
"FunctionDefinition",
|
|
38
|
+
"FunctionDefinitionError",
|
|
39
|
+
"FunctionDiscoveryError",
|
|
40
|
+
"FunctionEffect",
|
|
41
|
+
"FunctionError",
|
|
42
|
+
"FunctionExecutionError",
|
|
43
|
+
"FunctionBundle",
|
|
44
|
+
"FunctionMetadata",
|
|
45
|
+
"FunctionNotFoundError",
|
|
46
|
+
"FunctionOutputSerializationError",
|
|
47
|
+
"FunctionValidationError",
|
|
48
|
+
"build_manifest",
|
|
49
|
+
"build_bundle",
|
|
50
|
+
"discover_modules",
|
|
51
|
+
"function",
|
|
52
|
+
"get_function_definition",
|
|
53
|
+
"invoke_function",
|
|
54
|
+
"list_function_definitions",
|
|
55
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Build immutable, deterministic deployment bundles for Lumera Functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
from collections.abc import Iterator, Sequence
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ._core import build_manifest, discover_modules
|
|
19
|
+
|
|
20
|
+
FUNCTION_BUNDLE_FORMAT_VERSION = 1
|
|
21
|
+
FUNCTION_BUNDLE_RUNTIME = "python3.13"
|
|
22
|
+
|
|
23
|
+
_SOURCE_FILENAMES = {
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"requirements.txt",
|
|
26
|
+
"requirements.lock",
|
|
27
|
+
"uv.lock",
|
|
28
|
+
}
|
|
29
|
+
_EXCLUDED_DIRECTORY_NAMES = {
|
|
30
|
+
".git",
|
|
31
|
+
".mypy_cache",
|
|
32
|
+
".pytest_cache",
|
|
33
|
+
".ruff_cache",
|
|
34
|
+
".venv",
|
|
35
|
+
"__pycache__",
|
|
36
|
+
"node_modules",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class FunctionBundle:
|
|
42
|
+
"""Metadata for one built Function deployment bundle."""
|
|
43
|
+
|
|
44
|
+
path: Path
|
|
45
|
+
sha256: str
|
|
46
|
+
size_bytes: int
|
|
47
|
+
manifest: dict[str, Any]
|
|
48
|
+
modules: tuple[str, ...]
|
|
49
|
+
source_files: tuple[str, ...]
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
"""Return a JSON-serializable build result."""
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"path": str(self.path),
|
|
56
|
+
"sha256": self.sha256,
|
|
57
|
+
"size_bytes": self.size_bytes,
|
|
58
|
+
"manifest": self.manifest,
|
|
59
|
+
"modules": list(self.modules),
|
|
60
|
+
"source_files": list(self.source_files),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_bundle(
|
|
65
|
+
project_root: str | os.PathLike[str],
|
|
66
|
+
module_names: Sequence[str],
|
|
67
|
+
output_path: str | os.PathLike[str],
|
|
68
|
+
) -> FunctionBundle:
|
|
69
|
+
"""Build a deterministic ``tar.gz`` containing source and Function metadata."""
|
|
70
|
+
|
|
71
|
+
root = Path(project_root).expanduser().resolve()
|
|
72
|
+
if not root.is_dir():
|
|
73
|
+
raise ValueError(f"Function project root is not a directory: {root}")
|
|
74
|
+
|
|
75
|
+
modules = _normalize_modules(module_names)
|
|
76
|
+
output = Path(output_path).expanduser().resolve()
|
|
77
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
with _prepend_sys_path(root):
|
|
80
|
+
discover_modules(modules)
|
|
81
|
+
manifest = build_manifest()
|
|
82
|
+
|
|
83
|
+
source_entries = _collect_source_entries(root)
|
|
84
|
+
manifest_bytes = _canonical_json_bytes(manifest)
|
|
85
|
+
build_metadata = {
|
|
86
|
+
"format_version": FUNCTION_BUNDLE_FORMAT_VERSION,
|
|
87
|
+
"runtime": FUNCTION_BUNDLE_RUNTIME,
|
|
88
|
+
"modules": list(modules),
|
|
89
|
+
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
|
|
90
|
+
"source_files": [
|
|
91
|
+
{
|
|
92
|
+
"path": relative_path,
|
|
93
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
94
|
+
"size_bytes": len(content),
|
|
95
|
+
}
|
|
96
|
+
for relative_path, content in source_entries
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
archive_bytes = _build_archive(
|
|
101
|
+
manifest_bytes=manifest_bytes,
|
|
102
|
+
build_bytes=_canonical_json_bytes(build_metadata),
|
|
103
|
+
source_entries=source_entries,
|
|
104
|
+
)
|
|
105
|
+
output.write_bytes(archive_bytes)
|
|
106
|
+
|
|
107
|
+
return FunctionBundle(
|
|
108
|
+
path=output,
|
|
109
|
+
sha256=hashlib.sha256(archive_bytes).hexdigest(),
|
|
110
|
+
size_bytes=len(archive_bytes),
|
|
111
|
+
manifest=manifest,
|
|
112
|
+
modules=modules,
|
|
113
|
+
source_files=tuple(relative_path for relative_path, _ in source_entries),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _normalize_modules(module_names: Sequence[str]) -> tuple[str, ...]:
|
|
118
|
+
modules = tuple(dict.fromkeys(name.strip() for name in module_names if name.strip()))
|
|
119
|
+
if not modules:
|
|
120
|
+
raise ValueError("at least one Function module is required")
|
|
121
|
+
return modules
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _collect_source_entries(root: Path) -> list[tuple[str, bytes]]:
|
|
125
|
+
entries: list[tuple[str, bytes]] = []
|
|
126
|
+
for candidate in sorted(root.rglob("*")):
|
|
127
|
+
relative = candidate.relative_to(root)
|
|
128
|
+
if any(part in _EXCLUDED_DIRECTORY_NAMES for part in relative.parts):
|
|
129
|
+
continue
|
|
130
|
+
if candidate.is_symlink() or not candidate.is_file():
|
|
131
|
+
continue
|
|
132
|
+
if candidate.suffix != ".py" and candidate.name not in _SOURCE_FILENAMES:
|
|
133
|
+
continue
|
|
134
|
+
relative_path = PurePosixPath(*relative.parts).as_posix()
|
|
135
|
+
entries.append((relative_path, candidate.read_bytes()))
|
|
136
|
+
if not entries:
|
|
137
|
+
raise ValueError(f"Function project contains no Python source files: {root}")
|
|
138
|
+
return entries
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_archive(
|
|
142
|
+
*,
|
|
143
|
+
manifest_bytes: bytes,
|
|
144
|
+
build_bytes: bytes,
|
|
145
|
+
source_entries: Sequence[tuple[str, bytes]],
|
|
146
|
+
) -> bytes:
|
|
147
|
+
tar_buffer = io.BytesIO()
|
|
148
|
+
with tarfile.open(fileobj=tar_buffer, mode="w", format=tarfile.PAX_FORMAT) as archive:
|
|
149
|
+
_add_tar_file(archive, "manifest.json", manifest_bytes)
|
|
150
|
+
_add_tar_file(archive, "build.json", build_bytes)
|
|
151
|
+
for relative_path, content in source_entries:
|
|
152
|
+
_add_tar_file(archive, f"source/{relative_path}", content)
|
|
153
|
+
|
|
154
|
+
gzip_buffer = io.BytesIO()
|
|
155
|
+
with gzip.GzipFile(fileobj=gzip_buffer, mode="wb", filename="", mtime=0) as compressed:
|
|
156
|
+
compressed.write(tar_buffer.getvalue())
|
|
157
|
+
return gzip_buffer.getvalue()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _add_tar_file(archive: tarfile.TarFile, name: str, content: bytes) -> None:
|
|
161
|
+
info = tarfile.TarInfo(name)
|
|
162
|
+
info.size = len(content)
|
|
163
|
+
info.mode = 0o644
|
|
164
|
+
info.mtime = 0
|
|
165
|
+
info.uid = 0
|
|
166
|
+
info.gid = 0
|
|
167
|
+
info.uname = ""
|
|
168
|
+
info.gname = ""
|
|
169
|
+
archive.addfile(info, io.BytesIO(content))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _canonical_json_bytes(value: object) -> bytes:
|
|
173
|
+
return (
|
|
174
|
+
json.dumps(
|
|
175
|
+
value,
|
|
176
|
+
ensure_ascii=False,
|
|
177
|
+
separators=(",", ":"),
|
|
178
|
+
sort_keys=True,
|
|
179
|
+
)
|
|
180
|
+
+ "\n"
|
|
181
|
+
).encode()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@contextmanager
|
|
185
|
+
def _prepend_sys_path(root: Path) -> Iterator[None]:
|
|
186
|
+
root_value = str(root)
|
|
187
|
+
sys.path.insert(0, root_value)
|
|
188
|
+
try:
|
|
189
|
+
yield
|
|
190
|
+
finally:
|
|
191
|
+
try:
|
|
192
|
+
sys.path.remove(root_value)
|
|
193
|
+
except ValueError:
|
|
194
|
+
pass
|