salesforce-data-customcode 6.0.6.dev2__tar.gz → 6.0.6.dev3__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.
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/PKG-INFO +1 -1
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/pyproject.toml +1 -1
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/cli.py +29 -4
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/constants.py +8 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/deploy.py +55 -16
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/scan.py +164 -29
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/template.py +13 -1
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/LICENSE.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/README.md +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/client.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/config.yaml +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_platform_config.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/errors.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/spark_base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/spark_default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/einstein_predictions_config.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/feature_types/chunking.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function/runtime.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/errors.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/llm_gateway_config.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/run.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/payload/utility.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/examples/streaming_deltas/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/token_provider.py +0 -0
- {salesforce_data_customcode-6.0.6.dev2 → salesforce_data_customcode-6.0.6.dev3}/src/datacustomcode/version.py +0 -0
|
@@ -283,10 +283,19 @@ def deploy(
|
|
|
283
283
|
)
|
|
284
284
|
@click.option(
|
|
285
285
|
"--use-in-feature",
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
"-u",
|
|
287
|
+
default=None,
|
|
288
|
+
help=(
|
|
289
|
+
"Invoke option for this package. For scripts: 'BatchTransform' "
|
|
290
|
+
"(default) or 'StreamingTransform'. For functions: 'SearchIndexChunking'."
|
|
291
|
+
),
|
|
288
292
|
)
|
|
289
293
|
def init(directory: str, code_type: str, use_in_feature: Optional[str]):
|
|
294
|
+
from datacustomcode.constants import (
|
|
295
|
+
SCRIPT_USE_IN_FEATURE_BATCH,
|
|
296
|
+
SCRIPT_USE_IN_FEATURE_OPTIONS,
|
|
297
|
+
SCRIPT_USE_IN_FEATURE_STREAMING,
|
|
298
|
+
)
|
|
290
299
|
from datacustomcode.scan import (
|
|
291
300
|
dc_config_json_from_file,
|
|
292
301
|
update_config,
|
|
@@ -294,9 +303,23 @@ def init(directory: str, code_type: str, use_in_feature: Optional[str]):
|
|
|
294
303
|
)
|
|
295
304
|
from datacustomcode.template import copy_function_template, copy_script_template
|
|
296
305
|
|
|
306
|
+
streaming = False
|
|
307
|
+
if code_type == "script":
|
|
308
|
+
use_in_feature = use_in_feature or SCRIPT_USE_IN_FEATURE_BATCH
|
|
309
|
+
if use_in_feature not in SCRIPT_USE_IN_FEATURE_OPTIONS:
|
|
310
|
+
click.secho(
|
|
311
|
+
f"Error: Invalid --use-in-feature '{use_in_feature}' for a "
|
|
312
|
+
f"script. Valid options: {', '.join(SCRIPT_USE_IN_FEATURE_OPTIONS)}.",
|
|
313
|
+
fg="red",
|
|
314
|
+
)
|
|
315
|
+
raise click.Abort()
|
|
316
|
+
streaming = use_in_feature == SCRIPT_USE_IN_FEATURE_STREAMING
|
|
317
|
+
else:
|
|
318
|
+
use_in_feature = use_in_feature or "SearchIndexChunking"
|
|
319
|
+
|
|
297
320
|
click.echo("Copying template to " + click.style(directory, fg="blue", bold=True))
|
|
298
321
|
if code_type == "script":
|
|
299
|
-
copy_script_template(directory)
|
|
322
|
+
copy_script_template(directory, streaming=streaming)
|
|
300
323
|
elif code_type == "function":
|
|
301
324
|
copy_function_template(directory, use_in_feature)
|
|
302
325
|
entrypoint_path = os.path.join(directory, PAYLOAD_DIR, ENTRYPOINT_FILE)
|
|
@@ -306,7 +329,9 @@ def init(directory: str, code_type: str, use_in_feature: Optional[str]):
|
|
|
306
329
|
sdk_config = {"type": code_type}
|
|
307
330
|
write_sdk_config(directory, sdk_config)
|
|
308
331
|
|
|
309
|
-
config_json = dc_config_json_from_file(
|
|
332
|
+
config_json = dc_config_json_from_file(
|
|
333
|
+
entrypoint_path, code_type, streaming=streaming
|
|
334
|
+
)
|
|
310
335
|
with open(config_location, "w") as f:
|
|
311
336
|
json.dump(config_json, f, indent=2)
|
|
312
337
|
|
|
@@ -38,6 +38,14 @@ USE_IN_FEATURE_MAPPING_FOR_CONNECT_API = {
|
|
|
38
38
|
"SearchIndexChunking": "UnstructuredChunking",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
# Script (data transform) invoke options
|
|
42
|
+
SCRIPT_USE_IN_FEATURE_BATCH = "BatchTransform"
|
|
43
|
+
SCRIPT_USE_IN_FEATURE_STREAMING = "StreamingTransform"
|
|
44
|
+
SCRIPT_USE_IN_FEATURE_OPTIONS = [
|
|
45
|
+
SCRIPT_USE_IN_FEATURE_BATCH,
|
|
46
|
+
SCRIPT_USE_IN_FEATURE_STREAMING,
|
|
47
|
+
]
|
|
48
|
+
|
|
41
49
|
# Pydantic request/response type names to feature names
|
|
42
50
|
REQUEST_TYPE_TO_FEATURE = {
|
|
43
51
|
"SearchIndexChunkingV1Request": "SearchIndexChunking",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import copy
|
|
17
18
|
from html import unescape
|
|
18
19
|
import json
|
|
19
20
|
import os
|
|
@@ -41,6 +42,7 @@ from datacustomcode.scan import find_base_directory, get_package_type
|
|
|
41
42
|
|
|
42
43
|
DATA_CUSTOM_CODE_PATH = "services/data/v63.0/ssot/data-custom-code"
|
|
43
44
|
DATA_TRANSFORMS_PATH = "services/data/v63.0/ssot/data-transforms"
|
|
45
|
+
DATA_CUSTOM_CODE_INVOKE_OPTIONS_PATH = "services/data/v67.0/ssot/data-custom-code"
|
|
44
46
|
WAIT_FOR_DEPLOYMENT_TIMEOUT = 3000
|
|
45
47
|
|
|
46
48
|
# Available compute types for Data Cloud deployments.
|
|
@@ -108,6 +110,7 @@ class CodeExtensionMetadata(BaseModel):
|
|
|
108
110
|
computeType: str
|
|
109
111
|
codeType: str
|
|
110
112
|
functionInvokeOptions: Union[list[str], None] = None
|
|
113
|
+
invokeOptions: Union[list[str], None] = None
|
|
111
114
|
|
|
112
115
|
def __init__(self, **data):
|
|
113
116
|
name = data.get("name", "")
|
|
@@ -200,7 +203,14 @@ def create_deployment(
|
|
|
200
203
|
access_token: AccessTokenResponse, metadata: CodeExtensionMetadata
|
|
201
204
|
) -> CreateDeploymentResponse:
|
|
202
205
|
"""Create a custom code deployment in the DataCloud."""
|
|
203
|
-
|
|
206
|
+
# invokeOptions only binds at v67.0; route there when it is set so the
|
|
207
|
+
# option isn't silently dropped. Everything else stays on v63.0.
|
|
208
|
+
code_custom_code_path = (
|
|
209
|
+
DATA_CUSTOM_CODE_INVOKE_OPTIONS_PATH
|
|
210
|
+
if metadata.invokeOptions
|
|
211
|
+
else DATA_CUSTOM_CODE_PATH
|
|
212
|
+
)
|
|
213
|
+
url = _join_strip_url(access_token.instance_url, code_custom_code_path)
|
|
204
214
|
body = dict[str, Any](
|
|
205
215
|
{
|
|
206
216
|
"label": metadata.name,
|
|
@@ -213,6 +223,8 @@ def create_deployment(
|
|
|
213
223
|
)
|
|
214
224
|
if metadata.functionInvokeOptions:
|
|
215
225
|
body["functionInvokeOptions"] = metadata.functionInvokeOptions
|
|
226
|
+
if metadata.invokeOptions:
|
|
227
|
+
body["invokeOptions"] = metadata.invokeOptions
|
|
216
228
|
logger.debug(f"Creating deployment {metadata.name}...")
|
|
217
229
|
try:
|
|
218
230
|
response = _make_api_call(
|
|
@@ -388,6 +400,30 @@ class DataTransformConfig(BaseConfig):
|
|
|
388
400
|
dataspace: str
|
|
389
401
|
permissions: Permissions
|
|
390
402
|
dataObjects: Optional[list[DataObject]] = None
|
|
403
|
+
streamingSource: Optional[StreamingSource] = None
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def is_streaming(self) -> bool:
|
|
407
|
+
return self.streamingSource is not None
|
|
408
|
+
|
|
409
|
+
@model_validator(mode="after")
|
|
410
|
+
def _validate_layers(self) -> "DataTransformConfig":
|
|
411
|
+
read_is_dlo = isinstance(self.permissions.read, DloPermission)
|
|
412
|
+
write_is_dlo = isinstance(self.permissions.write, DloPermission)
|
|
413
|
+
if self.is_streaming:
|
|
414
|
+
if not write_is_dlo:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"A streaming transform must write to a DLO "
|
|
417
|
+
"(permissions.write must be a 'dlo' entry)."
|
|
418
|
+
)
|
|
419
|
+
elif read_is_dlo != write_is_dlo:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
"permissions.read and permissions.write must both reference "
|
|
422
|
+
"DLOs or both reference DMOs (got "
|
|
423
|
+
f"read={type(self.permissions.read).__name__}, "
|
|
424
|
+
f"write={type(self.permissions.write).__name__})"
|
|
425
|
+
)
|
|
426
|
+
return self
|
|
391
427
|
|
|
392
428
|
|
|
393
429
|
class FunctionConfig(BaseConfig):
|
|
@@ -402,23 +438,15 @@ class DmoPermission(BaseModel):
|
|
|
402
438
|
dmo: list[str]
|
|
403
439
|
|
|
404
440
|
|
|
441
|
+
class StreamingSource(BaseModel):
|
|
442
|
+
type: str
|
|
443
|
+
name: str
|
|
444
|
+
|
|
445
|
+
|
|
405
446
|
class Permissions(BaseModel):
|
|
406
447
|
read: Union[DloPermission, DmoPermission]
|
|
407
448
|
write: Union[DloPermission, DmoPermission]
|
|
408
449
|
|
|
409
|
-
@model_validator(mode="after")
|
|
410
|
-
def _no_mixed_layers(self) -> "Permissions":
|
|
411
|
-
read_is_dlo = isinstance(self.read, DloPermission)
|
|
412
|
-
write_is_dlo = isinstance(self.write, DloPermission)
|
|
413
|
-
if read_is_dlo != write_is_dlo:
|
|
414
|
-
raise ValueError(
|
|
415
|
-
"permissions.read and permissions.write must both reference "
|
|
416
|
-
"DLOs or both reference DMOs (got "
|
|
417
|
-
f"read={type(self.read).__name__}, "
|
|
418
|
-
f"write={type(self.write).__name__})"
|
|
419
|
-
)
|
|
420
|
-
return self
|
|
421
|
-
|
|
422
450
|
|
|
423
451
|
def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
|
|
424
452
|
"""Return the list of object names regardless of layer (DLO or DMO)."""
|
|
@@ -490,7 +518,9 @@ def create_data_transform(
|
|
|
490
518
|
) -> dict:
|
|
491
519
|
"""Create a data transform in the DataCloud."""
|
|
492
520
|
script_name = metadata.name
|
|
493
|
-
|
|
521
|
+
# Deep copy: the template's nested nodes/sources/macros dicts would
|
|
522
|
+
# otherwise be shared across calls and accumulate entries between deploys.
|
|
523
|
+
request_hydrated = copy.deepcopy(DATA_TRANSFORM_REQUEST_TEMPLATE)
|
|
494
524
|
|
|
495
525
|
# Add nodes for each write entry (DLO or DMO)
|
|
496
526
|
for i, name in enumerate(
|
|
@@ -533,7 +563,7 @@ def create_data_transform(
|
|
|
533
563
|
"definition": definition,
|
|
534
564
|
"label": f"{metadata.name}",
|
|
535
565
|
"name": f"{metadata.name}",
|
|
536
|
-
"type": "BATCH",
|
|
566
|
+
"type": "STREAMING" if data_transform_config.is_streaming else "BATCH",
|
|
537
567
|
"dataSpaceName": data_transform_config.dataspace,
|
|
538
568
|
}
|
|
539
569
|
|
|
@@ -616,9 +646,18 @@ def deploy_full(
|
|
|
616
646
|
callback=None,
|
|
617
647
|
) -> AccessTokenResponse:
|
|
618
648
|
"""Deploy a data transform in the DataCloud."""
|
|
649
|
+
from datacustomcode.constants import SCRIPT_USE_IN_FEATURE_STREAMING
|
|
650
|
+
|
|
619
651
|
# prepare payload
|
|
620
652
|
config = get_config(directory)
|
|
621
653
|
|
|
654
|
+
if (
|
|
655
|
+
isinstance(config, DataTransformConfig)
|
|
656
|
+
and config.is_streaming
|
|
657
|
+
and not metadata.invokeOptions
|
|
658
|
+
):
|
|
659
|
+
metadata.invokeOptions = [SCRIPT_USE_IN_FEATURE_STREAMING]
|
|
660
|
+
|
|
622
661
|
# create deployment and upload payload
|
|
623
662
|
deployment = create_deployment(access_token, metadata)
|
|
624
663
|
zip(directory, docker_network, metadata.codeType)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import ast
|
|
18
|
+
import copy
|
|
18
19
|
import json
|
|
19
20
|
import os
|
|
20
21
|
import sys
|
|
@@ -32,6 +33,8 @@ import pydantic
|
|
|
32
33
|
from datacustomcode.version import get_version
|
|
33
34
|
|
|
34
35
|
DATA_ACCESS_METHODS = ["read_dlo", "read_dmo", "write_to_dlo", "write_to_dmo"]
|
|
36
|
+
STREAMING_READ_METHODS = ["read_dlo_deltas", "read_dmo_deltas"]
|
|
37
|
+
STREAMING_WRITE_METHODS = ["write_dlo_deltas"]
|
|
35
38
|
|
|
36
39
|
DATA_TRANSFORM_CONFIG_TEMPLATE = {
|
|
37
40
|
"sdkVersion": get_version(),
|
|
@@ -43,6 +46,20 @@ DATA_TRANSFORM_CONFIG_TEMPLATE = {
|
|
|
43
46
|
},
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
STREAMING_TRANSFORM_CONFIG_TEMPLATE = {
|
|
50
|
+
"sdkVersion": get_version(),
|
|
51
|
+
"entryPoint": "",
|
|
52
|
+
"dataspace": "default",
|
|
53
|
+
"streamingSource": {
|
|
54
|
+
"type": "dlo",
|
|
55
|
+
"name": "",
|
|
56
|
+
},
|
|
57
|
+
"permissions": {
|
|
58
|
+
"read": {},
|
|
59
|
+
"write": {},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
FUNCTION_CONFIG_TEMPLATE = {
|
|
47
64
|
"entryPoint": "",
|
|
48
65
|
}
|
|
@@ -160,6 +177,35 @@ class DataAccessLayerCalls(pydantic.BaseModel):
|
|
|
160
177
|
return next(iter(self.write_to_dmo))
|
|
161
178
|
|
|
162
179
|
|
|
180
|
+
class StreamingDataAccessLayerCalls(pydantic.BaseModel):
|
|
181
|
+
read_dlo_deltas: bool
|
|
182
|
+
read_dmo_deltas: bool
|
|
183
|
+
write_dlo_deltas: frozenset[str]
|
|
184
|
+
|
|
185
|
+
@pydantic.model_validator(mode="after")
|
|
186
|
+
def validate_access_layer(self) -> StreamingDataAccessLayerCalls:
|
|
187
|
+
if self.read_dlo_deltas and self.read_dmo_deltas:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"Cannot read DLO and DMO deltas in the same streaming transform."
|
|
190
|
+
)
|
|
191
|
+
if not self.read_dlo_deltas and not self.read_dmo_deltas:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"A streaming transform must read from at least one DLO or DMO "
|
|
194
|
+
"delta stream (read_dlo_deltas / read_dmo_deltas)."
|
|
195
|
+
)
|
|
196
|
+
if not self.write_dlo_deltas:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"A streaming transform must write to at least one DLO via "
|
|
199
|
+
"write_dlo_deltas."
|
|
200
|
+
)
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def read_layer(self) -> str:
|
|
205
|
+
"""Return the read source layer, ``"dlo"`` or ``"dmo"``."""
|
|
206
|
+
return "dlo" if self.read_dlo_deltas else "dmo"
|
|
207
|
+
|
|
208
|
+
|
|
163
209
|
class ClientMethodVisitor(ast.NodeVisitor):
|
|
164
210
|
"""AST Visitor that finds all instances of Client read/write method calls."""
|
|
165
211
|
|
|
@@ -168,6 +214,9 @@ class ClientMethodVisitor(ast.NodeVisitor):
|
|
|
168
214
|
self._read_dmo_instances: set[str] = set()
|
|
169
215
|
self._write_to_dlo_instances: set[str] = set()
|
|
170
216
|
self._write_to_dmo_instances: set[str] = set()
|
|
217
|
+
self._read_dlo_deltas: bool = False
|
|
218
|
+
self._read_dmo_deltas: bool = False
|
|
219
|
+
self._write_dlo_deltas_instances: set[str] = set()
|
|
171
220
|
self.variable_values: Dict[str, Union[str, None]] = {}
|
|
172
221
|
|
|
173
222
|
def visit_Assign(self, node: ast.Assign) -> None:
|
|
@@ -189,14 +238,15 @@ class ClientMethodVisitor(ast.NodeVisitor):
|
|
|
189
238
|
node.func.value, ast.Name
|
|
190
239
|
):
|
|
191
240
|
method_name = node.func.attr
|
|
241
|
+
|
|
242
|
+
if method_name == "read_dlo_deltas":
|
|
243
|
+
self._read_dlo_deltas = True
|
|
244
|
+
elif method_name == "read_dmo_deltas":
|
|
245
|
+
self._read_dmo_deltas = True
|
|
246
|
+
|
|
192
247
|
if method_name in DATA_ACCESS_METHODS and node.args:
|
|
193
248
|
arg = node.args[0]
|
|
194
|
-
name =
|
|
195
|
-
|
|
196
|
-
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
197
|
-
name = arg.value
|
|
198
|
-
elif isinstance(arg, ast.Name) and arg.id in self.variable_values:
|
|
199
|
-
name = self.variable_values[arg.id]
|
|
249
|
+
name = self._resolve_name_arg(arg)
|
|
200
250
|
|
|
201
251
|
if name:
|
|
202
252
|
if method_name == "read_dlo":
|
|
@@ -207,8 +257,29 @@ class ClientMethodVisitor(ast.NodeVisitor):
|
|
|
207
257
|
self._write_to_dlo_instances.add(name)
|
|
208
258
|
elif method_name == "write_to_dmo":
|
|
209
259
|
self._write_to_dmo_instances.add(name)
|
|
260
|
+
elif method_name in STREAMING_WRITE_METHODS and node.args:
|
|
261
|
+
name = self._resolve_name_arg(node.args[0])
|
|
262
|
+
if name and method_name == "write_dlo_deltas":
|
|
263
|
+
self._write_dlo_deltas_instances.add(name)
|
|
210
264
|
self.generic_visit(node)
|
|
211
265
|
|
|
266
|
+
def _resolve_name_arg(self, arg: ast.expr) -> Union[str, None]:
|
|
267
|
+
"""Resolve a string-literal or tracked-variable first argument."""
|
|
268
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
269
|
+
return arg.value
|
|
270
|
+
if isinstance(arg, ast.Name) and arg.id in self.variable_values:
|
|
271
|
+
return self.variable_values[arg.id]
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def is_streaming(self) -> bool:
|
|
276
|
+
"""Whether any streaming (delta) access method was found."""
|
|
277
|
+
return (
|
|
278
|
+
self._read_dlo_deltas
|
|
279
|
+
or self._read_dmo_deltas
|
|
280
|
+
or bool(self._write_dlo_deltas_instances)
|
|
281
|
+
)
|
|
282
|
+
|
|
212
283
|
def found(self) -> DataAccessLayerCalls:
|
|
213
284
|
return DataAccessLayerCalls(
|
|
214
285
|
read_dlo=frozenset(self._read_dlo_instances),
|
|
@@ -217,6 +288,13 @@ class ClientMethodVisitor(ast.NodeVisitor):
|
|
|
217
288
|
write_to_dmo=frozenset(self._write_to_dmo_instances),
|
|
218
289
|
)
|
|
219
290
|
|
|
291
|
+
def found_streaming(self) -> StreamingDataAccessLayerCalls:
|
|
292
|
+
return StreamingDataAccessLayerCalls(
|
|
293
|
+
read_dlo_deltas=self._read_dlo_deltas,
|
|
294
|
+
read_dmo_deltas=self._read_dmo_deltas,
|
|
295
|
+
write_dlo_deltas=frozenset(self._write_dlo_deltas_instances),
|
|
296
|
+
)
|
|
297
|
+
|
|
220
298
|
|
|
221
299
|
class ImportVisitor(ast.NodeVisitor):
|
|
222
300
|
"""AST Visitor that extracts external package imports from Python code."""
|
|
@@ -301,23 +379,51 @@ def write_requirements_file(file_path: str) -> str:
|
|
|
301
379
|
return requirements_path
|
|
302
380
|
|
|
303
381
|
|
|
304
|
-
def
|
|
305
|
-
"""
|
|
382
|
+
def _visit_file(file_path: str) -> ClientMethodVisitor:
|
|
383
|
+
"""Parse a Python file and return the populated method visitor."""
|
|
306
384
|
with open(file_path, "r") as f:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
385
|
+
tree = ast.parse(f.read())
|
|
386
|
+
visitor = ClientMethodVisitor()
|
|
387
|
+
visitor.visit(tree)
|
|
388
|
+
return visitor
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def scan_file(file_path: str) -> DataAccessLayerCalls:
|
|
392
|
+
"""Scan a single Python file for batch Client read/write method calls."""
|
|
393
|
+
return _visit_file(file_path).found()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def scan_file_streaming(file_path: str) -> StreamingDataAccessLayerCalls:
|
|
397
|
+
"""Scan a single Python file for StreamingClient delta method calls."""
|
|
398
|
+
return _visit_file(file_path).found_streaming()
|
|
399
|
+
|
|
312
400
|
|
|
401
|
+
def file_is_streaming(file_path: str) -> bool:
|
|
402
|
+
"""Return whether the entrypoint uses streaming (delta) access methods."""
|
|
403
|
+
return _visit_file(file_path).is_streaming
|
|
313
404
|
|
|
314
|
-
|
|
315
|
-
|
|
405
|
+
|
|
406
|
+
def dc_config_json_from_file(
|
|
407
|
+
file_path: str, type: str, streaming: bool = False
|
|
408
|
+
) -> dict[str, Any]:
|
|
409
|
+
"""Create a Data Cloud Custom Code config JSON from a script.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
file_path: Path to the entrypoint.
|
|
413
|
+
type: Package type, ``"script"`` or ``"function"``.
|
|
414
|
+
streaming: For scripts, a streaming
|
|
415
|
+
(``streamingSource``) config instead of a batch one.
|
|
416
|
+
"""
|
|
316
417
|
config: dict[str, Any]
|
|
317
418
|
if type == "script":
|
|
318
|
-
|
|
419
|
+
template = (
|
|
420
|
+
STREAMING_TRANSFORM_CONFIG_TEMPLATE
|
|
421
|
+
if streaming
|
|
422
|
+
else DATA_TRANSFORM_CONFIG_TEMPLATE
|
|
423
|
+
)
|
|
424
|
+
config = copy.deepcopy(template)
|
|
319
425
|
elif type == "function":
|
|
320
|
-
config =
|
|
426
|
+
config = copy.deepcopy(FUNCTION_CONFIG_TEMPLATE)
|
|
321
427
|
config["entryPoint"] = os.path.basename(file_path)
|
|
322
428
|
return config
|
|
323
429
|
|
|
@@ -372,22 +478,51 @@ def update_config(file_path: str) -> dict[str, Any]:
|
|
|
372
478
|
|
|
373
479
|
if package_type == "script":
|
|
374
480
|
existing_config["dataspace"] = get_dataspace(existing_config)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if output.read_dlo:
|
|
378
|
-
read["dlo"] = list(output.read_dlo)
|
|
379
|
-
else:
|
|
380
|
-
read["dmo"] = list(output.read_dmo)
|
|
381
|
-
write: dict[str, list[str]] = {}
|
|
382
|
-
if output.write_to_dlo:
|
|
383
|
-
write["dlo"] = list(output.write_to_dlo)
|
|
481
|
+
if file_is_streaming(file_path):
|
|
482
|
+
_update_streaming_config(existing_config, file_path)
|
|
384
483
|
else:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
484
|
+
existing_config.pop("streamingSource", None)
|
|
485
|
+
output = scan_file(file_path)
|
|
486
|
+
read: dict[str, list[str]] = {}
|
|
487
|
+
if output.read_dlo:
|
|
488
|
+
read["dlo"] = list(output.read_dlo)
|
|
489
|
+
else:
|
|
490
|
+
read["dmo"] = list(output.read_dmo)
|
|
491
|
+
write: dict[str, list[str]] = {}
|
|
492
|
+
if output.write_to_dlo:
|
|
493
|
+
write["dlo"] = list(output.write_to_dlo)
|
|
494
|
+
else:
|
|
495
|
+
write["dmo"] = list(output.write_to_dmo)
|
|
496
|
+
|
|
497
|
+
existing_config["permissions"] = {"read": read, "write": write}
|
|
388
498
|
return existing_config
|
|
389
499
|
|
|
390
500
|
|
|
501
|
+
def _update_streaming_config(existing_config: dict[str, Any], file_path: str) -> None:
|
|
502
|
+
output = scan_file_streaming(file_path)
|
|
503
|
+
read_layer = output.read_layer
|
|
504
|
+
|
|
505
|
+
source = existing_config.get("streamingSource")
|
|
506
|
+
if not isinstance(source, dict):
|
|
507
|
+
source = {}
|
|
508
|
+
source_name = source.get("name", "")
|
|
509
|
+
existing_config["streamingSource"] = {"type": read_layer, "name": source_name}
|
|
510
|
+
|
|
511
|
+
if not source_name:
|
|
512
|
+
logger.warning(
|
|
513
|
+
"streamingSource.name is empty in config.json. A streaming "
|
|
514
|
+
"transform must declare its read source; set streamingSource.name "
|
|
515
|
+
"to the DLO/DMO the transform reads from."
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
read_names = [source_name] if source_name else []
|
|
519
|
+
write_names = list(output.write_dlo_deltas)
|
|
520
|
+
existing_config["permissions"] = {
|
|
521
|
+
"read": {read_layer: read_names},
|
|
522
|
+
"write": {"dlo": write_names},
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
391
526
|
def get_dataspace(existing_config: dict[str, str]) -> str:
|
|
392
527
|
if "dataspace" in existing_config:
|
|
393
528
|
dataspace_value = existing_config["dataspace"]
|
|
@@ -23,8 +23,12 @@ from datacustomcode.constants import FEATURE_TEMPLATE_MAPPING
|
|
|
23
23
|
script_template_dir = os.path.join(os.path.dirname(__file__), "templates", "script")
|
|
24
24
|
function_template_dir = os.path.join(os.path.dirname(__file__), "templates", "function")
|
|
25
25
|
|
|
26
|
+
STREAMING_EXAMPLE_ENTRYPOINT = os.path.join(
|
|
27
|
+
script_template_dir, "examples", "streaming_deltas", "entrypoint.py"
|
|
28
|
+
)
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
def copy_script_template(target_dir: str, streaming: bool = False) -> None:
|
|
28
32
|
"""Copy the template to the target directory."""
|
|
29
33
|
os.makedirs(target_dir, exist_ok=True)
|
|
30
34
|
|
|
@@ -39,6 +43,14 @@ def copy_script_template(target_dir: str) -> None:
|
|
|
39
43
|
logger.debug(f"Copying file {source} to {destination}...")
|
|
40
44
|
shutil.copy2(source, destination)
|
|
41
45
|
|
|
46
|
+
if streaming:
|
|
47
|
+
destination = os.path.join(target_dir, "payload", "entrypoint.py")
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Copying streaming example {STREAMING_EXAMPLE_ENTRYPOINT} to "
|
|
50
|
+
f"{destination}..."
|
|
51
|
+
)
|
|
52
|
+
shutil.copy2(STREAMING_EXAMPLE_ENTRYPOINT, destination)
|
|
53
|
+
|
|
42
54
|
|
|
43
55
|
def copy_function_template(target_dir: str, use_in_feature: Optional[str]) -> None:
|
|
44
56
|
os.makedirs(target_dir, exist_ok=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|