ob-metaflow-extensions 1.1.151__py2.py3-none-any.whl → 1.6.2__py2.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.
- metaflow_extensions/outerbounds/__init__.py +1 -1
- metaflow_extensions/outerbounds/plugins/__init__.py +24 -3
- metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +16 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +506 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/__init__.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/spinners.py +478 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +128 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_deploy_decorator.py +333 -0
- metaflow_extensions/outerbounds/plugins/apps/core/artifacts.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +1029 -0
- metaflow_extensions/outerbounds/plugins/apps/core/click_importer.py +24 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/__init__.py +3 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +618 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/examples.py +125 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +15 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +165 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +966 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +299 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +537 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +1125 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +337 -0
- metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +115 -0
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +1300 -0
- metaflow_extensions/outerbounds/plugins/apps/core/exceptions.py +341 -0
- metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +89 -0
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +123 -0
- metaflow_extensions/outerbounds/plugins/apps/core/secrets.py +164 -0
- metaflow_extensions/outerbounds/plugins/apps/core/utils.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/validations.py +17 -0
- metaflow_extensions/outerbounds/plugins/aws/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role.py +3 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +118 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +9 -77
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/external_chckpt.py +85 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +7 -78
- metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +119 -0
- metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +17 -3
- metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +1 -0
- metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +18 -44
- metaflow_extensions/outerbounds/plugins/kubernetes/pod_killer.py +374 -0
- metaflow_extensions/outerbounds/plugins/nim/card.py +1 -6
- metaflow_extensions/outerbounds/plugins/nim/{__init__.py → nim_decorator.py} +13 -49
- metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +294 -233
- metaflow_extensions/outerbounds/plugins/nim/utils.py +36 -0
- metaflow_extensions/outerbounds/plugins/nvcf/constants.py +2 -2
- metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +32 -8
- metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +1 -1
- metaflow_extensions/outerbounds/plugins/ollama/__init__.py +171 -16
- metaflow_extensions/outerbounds/plugins/ollama/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/ollama/exceptions.py +22 -0
- metaflow_extensions/outerbounds/plugins/ollama/ollama.py +1710 -114
- metaflow_extensions/outerbounds/plugins/ollama/status_card.py +292 -0
- metaflow_extensions/outerbounds/plugins/optuna/__init__.py +49 -0
- metaflow_extensions/outerbounds/plugins/profilers/simple_card_decorator.py +96 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/__init__.py +7 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/binary_caller.py +132 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/constants.py +11 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/exceptions.py +13 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/proxy_bootstrap.py +59 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_api.py +93 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_decorator.py +250 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_manager.py +225 -0
- metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +37 -7
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +18 -8
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +6 -0
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +45 -18
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +18 -9
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +10 -4
- metaflow_extensions/outerbounds/plugins/torchtune/__init__.py +163 -0
- metaflow_extensions/outerbounds/plugins/vllm/__init__.py +255 -0
- metaflow_extensions/outerbounds/plugins/vllm/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/exceptions.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/status_card.py +352 -0
- metaflow_extensions/outerbounds/plugins/vllm/vllm_manager.py +621 -0
- metaflow_extensions/outerbounds/remote_config.py +46 -9
- metaflow_extensions/outerbounds/toplevel/apps/__init__.py +9 -0
- metaflow_extensions/outerbounds/toplevel/apps/exceptions.py +11 -0
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +86 -2
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +4 -0
- metaflow_extensions/outerbounds/toplevel/plugins/optuna/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/torchtune/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/vllm/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/s3_proxy.py +88 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/METADATA +2 -2
- ob_metaflow_extensions-1.6.2.dist-info/RECORD +136 -0
- metaflow_extensions/outerbounds/plugins/nim/utilities.py +0 -5
- ob_metaflow_extensions-1.1.151.dist-info/RECORD +0 -74
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from metaflow.exception import MetaflowException
|
|
3
|
+
from metaflow.decorators import StepDecorator, FlowDecorator
|
|
4
|
+
from metaflow import current
|
|
5
|
+
from metaflow.metaflow_config import KUBERNETES_CONTAINER_IMAGE
|
|
6
|
+
from .deployer import AppDeployer, DeployedApp, PackagedCode
|
|
7
|
+
from .perimeters import PerimeterExtractor
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AppDeployFlowDecorator(FlowDecorator):
|
|
12
|
+
"""
|
|
13
|
+
Deploy and manage Outerbounds Apps from within a Metaflow flow.
|
|
14
|
+
|
|
15
|
+
This decorator enables deploying apps during flow execution, automatically
|
|
16
|
+
tagging them with Metaflow metadata (flow name, run ID, step, etc.) for
|
|
17
|
+
easy tracking and management.
|
|
18
|
+
|
|
19
|
+
When applied to a flow, it exposes `current.apps` which provides:
|
|
20
|
+
- Access to the flow's code package and image
|
|
21
|
+
- Ability to list apps deployed in the current run
|
|
22
|
+
- Automatic tagging with Metaflow context
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from metaflow import FlowSpec, step, current, app_deploy
|
|
29
|
+
from metaflow.apps import AppDeployer
|
|
30
|
+
|
|
31
|
+
@app_deploy
|
|
32
|
+
class MyFlow(FlowSpec):
|
|
33
|
+
|
|
34
|
+
@step
|
|
35
|
+
def start(self):
|
|
36
|
+
# Deploy an app using the flow's code package
|
|
37
|
+
deployer = AppDeployer(
|
|
38
|
+
name="my-service",
|
|
39
|
+
port=8000,
|
|
40
|
+
image=current.apps.current_image,
|
|
41
|
+
code_package=current.apps.metaflow_code_package,
|
|
42
|
+
commands=["python server.py"],
|
|
43
|
+
)
|
|
44
|
+
self.app = deployer.deploy()
|
|
45
|
+
self.next(self.end)
|
|
46
|
+
|
|
47
|
+
@step
|
|
48
|
+
def end(self):
|
|
49
|
+
# List all apps deployed in this run
|
|
50
|
+
apps = current.apps.list()
|
|
51
|
+
print(f"Deployed {len(apps)} app(s)")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
MF Add To Current
|
|
56
|
+
-----------------
|
|
57
|
+
apps -> metaflow_extensions.outerbounds.plugins.apps.core.app_deploy_decorator.FlowAppManager
|
|
58
|
+
|
|
59
|
+
@@ Returns
|
|
60
|
+
----------
|
|
61
|
+
FlowAppManager
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
name = "app_deploy"
|
|
65
|
+
defaults = {}
|
|
66
|
+
|
|
67
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
68
|
+
self._attributes_with_user_values = (
|
|
69
|
+
set(attributes.keys()) if attributes is not None else set()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
super().__init__(attributes, statically_defined, inserted_by)
|
|
73
|
+
|
|
74
|
+
def flow_init(
|
|
75
|
+
self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
|
|
76
|
+
):
|
|
77
|
+
|
|
78
|
+
from metaflow import decorators
|
|
79
|
+
|
|
80
|
+
decorators._attach_decorators(flow, ["app_deploy_internal"])
|
|
81
|
+
decorators._process_late_attached_decorator(
|
|
82
|
+
["app_deploy_internal"],
|
|
83
|
+
flow,
|
|
84
|
+
graph,
|
|
85
|
+
environment,
|
|
86
|
+
flow_datastore,
|
|
87
|
+
logger,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AppDeployInternalDecorator(StepDecorator):
|
|
92
|
+
|
|
93
|
+
name = "app_deploy_internal"
|
|
94
|
+
defaults = {}
|
|
95
|
+
|
|
96
|
+
packaged_code = None
|
|
97
|
+
|
|
98
|
+
package_url = None
|
|
99
|
+
package_sha = None
|
|
100
|
+
|
|
101
|
+
MAX_ENTROPY = 6
|
|
102
|
+
MAX_NAME_LENGTH = 150
|
|
103
|
+
|
|
104
|
+
def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
|
|
105
|
+
self.logger = logger
|
|
106
|
+
self.environment = environment
|
|
107
|
+
self.step = step
|
|
108
|
+
self.flow_datastore = flow_datastore
|
|
109
|
+
|
|
110
|
+
def _resolve_package_url_and_sha(self):
|
|
111
|
+
return os.environ.get("METAFLOW_CODE_URL", self.package_url), os.environ.get(
|
|
112
|
+
"METAFLOW_CODE_SHA", self.package_sha
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _extract_project_info(self):
|
|
116
|
+
project = current.get("project_name")
|
|
117
|
+
branch = current.get("branch_name")
|
|
118
|
+
is_production = current.get("is_production")
|
|
119
|
+
return project, branch, is_production
|
|
120
|
+
|
|
121
|
+
def task_pre_step(
|
|
122
|
+
self,
|
|
123
|
+
step_name,
|
|
124
|
+
task_datastore,
|
|
125
|
+
metadata,
|
|
126
|
+
run_id,
|
|
127
|
+
task_id,
|
|
128
|
+
flow,
|
|
129
|
+
graph,
|
|
130
|
+
retry_count,
|
|
131
|
+
max_user_code_retries,
|
|
132
|
+
ubf_context,
|
|
133
|
+
inputs,
|
|
134
|
+
):
|
|
135
|
+
perimeter, api_server = PerimeterExtractor.during_programmatic_access()
|
|
136
|
+
package_url, package_sha = self._resolve_package_url_and_sha()
|
|
137
|
+
if package_url is None or package_sha is None:
|
|
138
|
+
# TODO: Think through if we need this abstraction
|
|
139
|
+
raise MetaflowException(
|
|
140
|
+
"METAFLOW_CODE_URL or METAFLOW_CODE_SHA is not set. "
|
|
141
|
+
"Please set METAFLOW_CODE_URL and METAFLOW_CODE_SHA in your environment."
|
|
142
|
+
)
|
|
143
|
+
self.packaged_code = PackagedCode(package_url, package_sha)
|
|
144
|
+
image = os.environ.get("FASTBAKERY_IMAGE", None)
|
|
145
|
+
|
|
146
|
+
project, branch, is_production = self._extract_project_info()
|
|
147
|
+
|
|
148
|
+
project_info = {}
|
|
149
|
+
if project is not None:
|
|
150
|
+
project_info["metaflow/project"] = project
|
|
151
|
+
project_info["metaflow/branch"] = branch
|
|
152
|
+
project_info["metaflow/is_production"] = is_production
|
|
153
|
+
|
|
154
|
+
default_tags = {
|
|
155
|
+
"metaflow/flow_name": flow.name,
|
|
156
|
+
"metaflow/step_name": step_name,
|
|
157
|
+
"metaflow/run_id": run_id,
|
|
158
|
+
"metaflow/task_id": task_id,
|
|
159
|
+
"metaflow/retry_count": retry_count,
|
|
160
|
+
"metaflow/pathspec": current.pathspec,
|
|
161
|
+
**project_info,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Set state on AppDeployer using the new API
|
|
165
|
+
AppDeployer._set_state("perimeter", perimeter)
|
|
166
|
+
AppDeployer._set_state("api_url", api_server)
|
|
167
|
+
AppDeployer._set_state(
|
|
168
|
+
"default_tags", [{k: str(v)} for k, v in default_tags.items()]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
current._update_env(
|
|
172
|
+
{
|
|
173
|
+
"apps": FlowAppManager(
|
|
174
|
+
flow.name,
|
|
175
|
+
run_id,
|
|
176
|
+
self.packaged_code,
|
|
177
|
+
image,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def task_post_step(
|
|
183
|
+
self, step_name, flow, graph, retry_count, max_user_code_retries
|
|
184
|
+
):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def runtime_init(self, flow, graph, package, run_id):
|
|
188
|
+
# Set some more internal state.
|
|
189
|
+
self.flow = flow
|
|
190
|
+
self.graph = graph
|
|
191
|
+
self.package = package
|
|
192
|
+
self.run_id = run_id
|
|
193
|
+
|
|
194
|
+
def runtime_task_created(
|
|
195
|
+
self, task_datastore, task_id, split_index, input_paths, is_cloned, ubf_context
|
|
196
|
+
):
|
|
197
|
+
# To execute the Kubernetes job, the job container needs to have
|
|
198
|
+
# access to the code package. We store the package in the datastore
|
|
199
|
+
# which the pod is able to download as part of it's entrypoint.
|
|
200
|
+
if not is_cloned:
|
|
201
|
+
self._save_package_once(self.flow_datastore, self.package)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def _save_package_once(cls, flow_datastore, package):
|
|
205
|
+
if cls.packaged_code is None:
|
|
206
|
+
cls.package_url, cls.package_sha = flow_datastore.save_data(
|
|
207
|
+
[package.blob], len_hint=1
|
|
208
|
+
)[0]
|
|
209
|
+
cls.packaged_code = PackagedCode(cls.package_url, cls.package_sha)
|
|
210
|
+
os.environ["METAFLOW_CODE_URL"] = cls.package_url
|
|
211
|
+
os.environ["METAFLOW_CODE_SHA"] = cls.package_sha
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class FlowAppManager:
|
|
215
|
+
"""
|
|
216
|
+
Manager for apps deployed within a Metaflow flow execution.
|
|
217
|
+
|
|
218
|
+
Accessible via `current.apps` when using the `@app_deploy` decorator.
|
|
219
|
+
Provides access to the flow's code package, container image, and
|
|
220
|
+
methods to list apps deployed in the current run.
|
|
221
|
+
|
|
222
|
+
Attributes
|
|
223
|
+
----------
|
|
224
|
+
metaflow_code_package : PackagedCode
|
|
225
|
+
The code package for the current flow, ready to use with AppDeployer.
|
|
226
|
+
current_image : str
|
|
227
|
+
The container image used by the current task (from fast_bakery or similar).
|
|
228
|
+
default_image : str
|
|
229
|
+
The default Kubernetes container image from Metaflow config.
|
|
230
|
+
|
|
231
|
+
Examples
|
|
232
|
+
--------
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
# python myflow.py --environment=fast-bakery run --with kubernetes
|
|
236
|
+
from metaflow.apps import AppDeployer
|
|
237
|
+
|
|
238
|
+
@pypi(packages={"flask": ">=2.0", "requests": ">=2.28"})
|
|
239
|
+
@step
|
|
240
|
+
def deploy_step(self):
|
|
241
|
+
image = current.apps.current_image
|
|
242
|
+
if image is None:
|
|
243
|
+
image = current.apps.default_image
|
|
244
|
+
# Use the flow's code package directly
|
|
245
|
+
deployer = AppDeployer(
|
|
246
|
+
name="my-app",
|
|
247
|
+
port=8000,
|
|
248
|
+
image=image,
|
|
249
|
+
code_package=current.apps.metaflow_code_package,
|
|
250
|
+
commands=["python app.py"],
|
|
251
|
+
)
|
|
252
|
+
deployed = deployer.deploy()
|
|
253
|
+
|
|
254
|
+
# List apps from this run
|
|
255
|
+
apps = current.apps.list()
|
|
256
|
+
```
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(
|
|
260
|
+
self,
|
|
261
|
+
flow_name: str,
|
|
262
|
+
run_id: str,
|
|
263
|
+
package: PackagedCode,
|
|
264
|
+
image: Optional[str] = None,
|
|
265
|
+
) -> None:
|
|
266
|
+
self._package = package
|
|
267
|
+
self._image = image
|
|
268
|
+
self._runid = run_id
|
|
269
|
+
self._flowname = flow_name
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def metaflow_code_package(self) -> PackagedCode:
|
|
273
|
+
"""
|
|
274
|
+
The flow's code package for use with AppDeployer.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
PackagedCode
|
|
279
|
+
A namedtuple with `url` and `key` fields pointing to the
|
|
280
|
+
packaged metaflow's code package stored in the datastore.
|
|
281
|
+
"""
|
|
282
|
+
return self._package
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def current_image(self) -> str:
|
|
286
|
+
"""
|
|
287
|
+
The container image for the current task.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
str
|
|
292
|
+
Image URI from fast bakery image or None if not set.
|
|
293
|
+
"""
|
|
294
|
+
return self._image
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def default_image(self) -> str:
|
|
298
|
+
"""
|
|
299
|
+
The default Kubernetes container image from Metaflow config.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
str
|
|
304
|
+
The KUBERNETES_CONTAINER_IMAGE from Metaflow configuration.
|
|
305
|
+
"""
|
|
306
|
+
return KUBERNETES_CONTAINER_IMAGE
|
|
307
|
+
|
|
308
|
+
def list(self) -> List["DeployedApp"]:
|
|
309
|
+
"""
|
|
310
|
+
List apps deployed in the current Metaflow run.
|
|
311
|
+
|
|
312
|
+
Returns apps tagged with this flow's name and run ID.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
List[DeployedApp]
|
|
317
|
+
Apps deployed during this flow execution.
|
|
318
|
+
|
|
319
|
+
Examples
|
|
320
|
+
--------
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
apps = current.apps.list()
|
|
324
|
+
for app in apps:
|
|
325
|
+
print(f"{app.name}: {app.public_url}")
|
|
326
|
+
```
|
|
327
|
+
"""
|
|
328
|
+
return AppDeployer.list_deployments(
|
|
329
|
+
tags=[
|
|
330
|
+
{"metaflow/run_id": self._runid},
|
|
331
|
+
{"metaflow/flow_name": self._flowname},
|
|
332
|
+
]
|
|
333
|
+
)
|
|
File without changes
|