mlrun 1.7.0rc39__py3-none-any.whl → 1.7.0rc41__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.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

Files changed (44) hide show
  1. mlrun/common/constants.py +3 -0
  2. mlrun/common/helpers.py +0 -1
  3. mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -1
  4. mlrun/config.py +1 -1
  5. mlrun/data_types/to_pandas.py +9 -9
  6. mlrun/datastore/alibaba_oss.py +1 -0
  7. mlrun/datastore/azure_blob.py +1 -6
  8. mlrun/datastore/base.py +12 -0
  9. mlrun/datastore/dbfs_store.py +1 -5
  10. mlrun/datastore/filestore.py +1 -3
  11. mlrun/datastore/google_cloud_storage.py +1 -9
  12. mlrun/datastore/redis.py +1 -0
  13. mlrun/datastore/s3.py +1 -0
  14. mlrun/datastore/storeytargets.py +147 -0
  15. mlrun/datastore/targets.py +67 -69
  16. mlrun/datastore/v3io.py +1 -0
  17. mlrun/model_monitoring/api.py +1 -2
  18. mlrun/model_monitoring/applications/_application_steps.py +25 -43
  19. mlrun/model_monitoring/applications/context.py +206 -70
  20. mlrun/model_monitoring/controller.py +0 -1
  21. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +17 -8
  22. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +14 -4
  23. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +11 -3
  24. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +35 -23
  25. mlrun/model_monitoring/helpers.py +38 -1
  26. mlrun/model_monitoring/stream_processing.py +8 -26
  27. mlrun/projects/project.py +17 -16
  28. mlrun/runtimes/nuclio/api_gateway.py +9 -0
  29. mlrun/runtimes/nuclio/application/application.py +131 -55
  30. mlrun/runtimes/nuclio/function.py +4 -10
  31. mlrun/runtimes/nuclio/serving.py +2 -2
  32. mlrun/runtimes/utils.py +16 -0
  33. mlrun/serving/routers.py +1 -1
  34. mlrun/serving/server.py +19 -5
  35. mlrun/serving/states.py +8 -0
  36. mlrun/serving/v2_serving.py +34 -26
  37. mlrun/utils/helpers.py +12 -2
  38. mlrun/utils/version/version.json +2 -2
  39. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/METADATA +2 -2
  40. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/RECORD +44 -43
  41. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/WHEEL +1 -1
  42. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/LICENSE +0 -0
  43. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/entry_points.txt +0 -0
  44. {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,7 @@ import mlrun.common.model_monitoring
24
24
  import mlrun.common.schemas.model_monitoring as mm_schemas
25
25
  import mlrun.feature_store.steps
26
26
  import mlrun.utils.v3io_clients
27
+ from mlrun.common.schemas import EventFieldType
27
28
  from mlrun.model_monitoring.db import TSDBConnector
28
29
  from mlrun.model_monitoring.helpers import get_invocations_fqn
29
30
  from mlrun.utils import logger
@@ -64,14 +65,17 @@ class V3IOTSDBConnector(TSDBConnector):
64
65
  self.container = container
65
66
 
66
67
  self.v3io_framesd = v3io_framesd or mlrun.mlconf.v3io_framesd
67
- self._frames_client: v3io_frames.client.ClientBase = (
68
- self._get_v3io_frames_client(self.container)
69
- )
70
-
68
+ self._frames_client: Optional[v3io_frames.client.ClientBase] = None
71
69
  self._init_tables_path()
70
+ self._create_table = create_table
72
71
 
73
- if create_table:
74
- self.create_tables()
72
+ @property
73
+ def frames_client(self) -> v3io_frames.client.ClientBase:
74
+ if not self._frames_client:
75
+ self._frames_client = self._get_v3io_frames_client(self.container)
76
+ if self._create_table:
77
+ self.create_tables()
78
+ return self._frames_client
75
79
 
76
80
  def _init_tables_path(self):
77
81
  self.tables = {}
@@ -151,7 +155,7 @@ class V3IOTSDBConnector(TSDBConnector):
151
155
  for table_name in application_tables:
152
156
  logger.info("Creating table in V3IO TSDB", table_name=table_name)
153
157
  table = self.tables[table_name]
154
- self._frames_client.create(
158
+ self.frames_client.create(
155
159
  backend=_TSDB_BE,
156
160
  table=table,
157
161
  if_exists=v3io_frames.IGNORE,
@@ -161,8 +165,9 @@ class V3IOTSDBConnector(TSDBConnector):
161
165
  def apply_monitoring_stream_steps(
162
166
  self,
163
167
  graph,
164
- tsdb_batching_max_events: int = 10,
165
- tsdb_batching_timeout_secs: int = 300,
168
+ tsdb_batching_max_events: int = 1000,
169
+ tsdb_batching_timeout_secs: int = 30,
170
+ sample_window: int = 10,
166
171
  ):
167
172
  """
168
173
  Apply TSDB steps on the provided monitoring graph. Throughout these steps, the graph stores live data of
@@ -173,6 +178,7 @@ class V3IOTSDBConnector(TSDBConnector):
173
178
  - endpoint_features (Prediction and feature names and values)
174
179
  - custom_metrics (user-defined metrics)
175
180
  """
181
+
176
182
  # Write latency per prediction, labeled by endpoint ID only
177
183
  graph.add_step(
178
184
  "storey.TSDBTarget",
@@ -197,17 +203,23 @@ class V3IOTSDBConnector(TSDBConnector):
197
203
  key=mm_schemas.EventFieldType.ENDPOINT_ID,
198
204
  )
199
205
 
206
+ # Emits the event in window size of events based on sample_window size (10 by default)
207
+ graph.add_step(
208
+ "storey.steps.SampleWindow",
209
+ name="sample",
210
+ after="Rename",
211
+ window_size=sample_window,
212
+ key=EventFieldType.ENDPOINT_ID,
213
+ )
214
+
200
215
  # Before writing data to TSDB, create dictionary of 2-3 dictionaries that contains
201
216
  # stats and details about the events
202
217
 
203
- def apply_process_before_tsdb():
204
- graph.add_step(
205
- "mlrun.model_monitoring.db.tsdb.v3io.stream_graph_steps.ProcessBeforeTSDB",
206
- name="ProcessBeforeTSDB",
207
- after="sample",
208
- )
209
-
210
- apply_process_before_tsdb()
218
+ graph.add_step(
219
+ "mlrun.model_monitoring.db.tsdb.v3io.stream_graph_steps.ProcessBeforeTSDB",
220
+ name="ProcessBeforeTSDB",
221
+ after="sample",
222
+ )
211
223
 
212
224
  # Unpacked keys from each dictionary and write to TSDB target
213
225
  def apply_filter_and_unpacked_keys(name, keys):
@@ -273,8 +285,8 @@ class V3IOTSDBConnector(TSDBConnector):
273
285
  def handle_model_error(
274
286
  self,
275
287
  graph,
276
- tsdb_batching_max_events: int = 10,
277
- tsdb_batching_timeout_secs: int = 60,
288
+ tsdb_batching_max_events: int = 1000,
289
+ tsdb_batching_timeout_secs: int = 30,
278
290
  **kwargs,
279
291
  ) -> None:
280
292
  graph.add_step(
@@ -333,7 +345,7 @@ class V3IOTSDBConnector(TSDBConnector):
333
345
  raise ValueError(f"Invalid {kind = }")
334
346
 
335
347
  try:
336
- self._frames_client.write(
348
+ self.frames_client.write(
337
349
  backend=_TSDB_BE,
338
350
  table=table,
339
351
  dfs=pd.DataFrame.from_records([event]),
@@ -360,7 +372,7 @@ class V3IOTSDBConnector(TSDBConnector):
360
372
  tables = mm_schemas.V3IOTSDBTables.list()
361
373
  for table_to_delete in tables:
362
374
  try:
363
- self._frames_client.delete(backend=_TSDB_BE, table=table_to_delete)
375
+ self.frames_client.delete(backend=_TSDB_BE, table=table_to_delete)
364
376
  except v3io_frames.DeleteError as e:
365
377
  logger.warning(
366
378
  f"Failed to delete TSDB table '{table}'",
@@ -476,7 +488,7 @@ class V3IOTSDBConnector(TSDBConnector):
476
488
  aggregators = ",".join(agg_funcs) if agg_funcs else None
477
489
  table_path = self.tables[table]
478
490
  try:
479
- df = self._frames_client.read(
491
+ df = self.frames_client.read(
480
492
  backend=_TSDB_BE,
481
493
  table=table_path,
482
494
  start=start,
@@ -579,7 +591,7 @@ class V3IOTSDBConnector(TSDBConnector):
579
591
 
580
592
  logger.debug("Querying V3IO TSDB", query=query)
581
593
 
582
- df: pd.DataFrame = self._frames_client.read(
594
+ df: pd.DataFrame = self.frames_client.read(
583
595
  backend=_TSDB_BE,
584
596
  start=start,
585
597
  end=end,
@@ -19,9 +19,11 @@ import numpy as np
19
19
  import pandas as pd
20
20
 
21
21
  import mlrun
22
+ import mlrun.artifacts
22
23
  import mlrun.common.model_monitoring.helpers
23
24
  import mlrun.common.schemas.model_monitoring.constants as mm_constants
24
25
  import mlrun.data_types.infer
26
+ import mlrun.model_monitoring
25
27
  from mlrun.common.schemas.model_monitoring.model_endpoints import (
26
28
  ModelEndpointMonitoringMetric,
27
29
  ModelEndpointMonitoringMetricType,
@@ -253,7 +255,7 @@ def calculate_inputs_statistics(
253
255
  )
254
256
 
255
257
  # Recalculate the histograms over the bins that are set in the sample-set of the end point:
256
- for feature in inputs_statistics.keys():
258
+ for feature in list(inputs_statistics):
257
259
  if feature in sample_set_statistics:
258
260
  counts, bins = np.histogram(
259
261
  inputs[feature].to_numpy(),
@@ -270,6 +272,9 @@ def calculate_inputs_statistics(
270
272
  inputs_statistics[feature]["hist"]
271
273
  )
272
274
  )
275
+ else:
276
+ # If the feature is not in the sample set and doesn't have a histogram, remove it from the statistics:
277
+ inputs_statistics.pop(feature)
273
278
 
274
279
  return inputs_statistics
275
280
 
@@ -322,3 +327,35 @@ def get_invocations_metric(project: str) -> ModelEndpointMonitoringMetric:
322
327
  name=mm_constants.PredictionsQueryConstants.INVOCATIONS,
323
328
  full_name=get_invocations_fqn(project),
324
329
  )
330
+
331
+
332
+ def enrich_model_endpoint_with_model_uri(
333
+ model_endpoint: ModelEndpoint,
334
+ model_obj: mlrun.artifacts.ModelArtifact,
335
+ ):
336
+ """
337
+ Enrich the model endpoint object with the model uri from the model object. We will use a unique reference
338
+ to the model object that includes the project, db_key, iter, and tree.
339
+ In addition, we verify that the model object is of type `ModelArtifact`.
340
+
341
+ :param model_endpoint: An object representing the model endpoint that will be enriched with the model uri.
342
+ :param model_obj: An object representing the model artifact.
343
+
344
+ :raise: `MLRunInvalidArgumentError` if the model object is not of type `ModelArtifact`.
345
+ """
346
+ mlrun.utils.helpers.verify_field_of_type(
347
+ field_name="model_endpoint.spec.model_uri",
348
+ field_value=model_obj,
349
+ expected_type=mlrun.artifacts.ModelArtifact,
350
+ )
351
+
352
+ # Update model_uri with a unique reference to handle future changes
353
+ model_artifact_uri = mlrun.utils.helpers.generate_artifact_uri(
354
+ project=model_endpoint.metadata.project,
355
+ key=model_obj.db_key,
356
+ iter=model_obj.iter,
357
+ tree=model_obj.tree,
358
+ )
359
+ model_endpoint.spec.model_uri = mlrun.datastore.get_store_uri(
360
+ kind=mlrun.utils.helpers.StorePrefix.Model, uri=model_artifact_uri
361
+ )
@@ -37,6 +37,7 @@ from mlrun.common.schemas.model_monitoring.constants import (
37
37
  ModelEndpointTarget,
38
38
  ProjectSecretKeys,
39
39
  )
40
+ from mlrun.model_monitoring.db import StoreBase, TSDBConnector
40
41
  from mlrun.utils import logger
41
42
 
42
43
 
@@ -48,14 +49,12 @@ class EventStreamProcessor:
48
49
  parquet_batching_max_events: int,
49
50
  parquet_batching_timeout_secs: int,
50
51
  parquet_target: str,
51
- sample_window: int = 10,
52
52
  aggregate_windows: typing.Optional[list[str]] = None,
53
- aggregate_period: str = "30s",
53
+ aggregate_period: str = "5m",
54
54
  model_monitoring_access_key: str = None,
55
55
  ):
56
56
  # General configurations, mainly used for the storey steps in the future serving graph
57
57
  self.project = project
58
- self.sample_window = sample_window
59
58
  self.aggregate_windows = aggregate_windows or ["5m", "1h"]
60
59
  self.aggregate_period = aggregate_period
61
60
 
@@ -133,7 +132,8 @@ class EventStreamProcessor:
133
132
  def apply_monitoring_serving_graph(
134
133
  self,
135
134
  fn: mlrun.runtimes.ServingRuntime,
136
- secret_provider: typing.Optional[typing.Callable[[str], str]] = None,
135
+ tsdb_connector: TSDBConnector,
136
+ endpoint_store: StoreBase,
137
137
  ) -> None:
138
138
  """
139
139
  Apply monitoring serving graph to a given serving function. The following serving graph includes about 4 main
@@ -161,8 +161,8 @@ class EventStreamProcessor:
161
161
  using CE, the parquet target path is based on the defined MLRun artifact path.
162
162
 
163
163
  :param fn: A serving function.
164
- :param secret_provider: An optional callable function that provides the connection string from the project
165
- secret.
164
+ :param tsdb_connector: Time series database connector.
165
+ :param endpoint_store: KV/SQL store used for endpoint data.
166
166
  """
167
167
 
168
168
  graph = typing.cast(
@@ -190,10 +190,6 @@ class EventStreamProcessor:
190
190
  _fn="(event.get('error') is not None)",
191
191
  )
192
192
 
193
- tsdb_connector = mlrun.model_monitoring.get_tsdb_connector(
194
- project=self.project, secret_provider=secret_provider
195
- )
196
-
197
193
  tsdb_connector.handle_model_error(
198
194
  graph,
199
195
  )
@@ -306,24 +302,9 @@ class EventStreamProcessor:
306
302
  table=self.kv_path,
307
303
  )
308
304
 
309
- store_object = mlrun.model_monitoring.get_store_object(
310
- project=self.project, secret_provider=secret_provider
311
- )
312
- if store_object.type == ModelEndpointTarget.V3IO_NOSQL:
305
+ if endpoint_store.type == ModelEndpointTarget.V3IO_NOSQL:
313
306
  apply_infer_schema()
314
307
 
315
- # Emits the event in window size of events based on sample_window size (10 by default)
316
- def apply_storey_sample_window():
317
- graph.add_step(
318
- "storey.steps.SampleWindow",
319
- name="sample",
320
- after="Rename",
321
- window_size=self.sample_window,
322
- key=EventFieldType.ENDPOINT_ID,
323
- )
324
-
325
- apply_storey_sample_window()
326
-
327
308
  tsdb_connector.apply_monitoring_stream_steps(graph=graph)
328
309
 
329
310
  # Parquet branch
@@ -353,6 +334,7 @@ class EventStreamProcessor:
353
334
  index_cols=[EventFieldType.ENDPOINT_ID],
354
335
  key_bucketing_number=0,
355
336
  time_partitioning_granularity="hour",
337
+ time_field=EventFieldType.TIMESTAMP,
356
338
  partition_cols=["$key", "$year", "$month", "$day", "$hour"],
357
339
  )
358
340
 
mlrun/projects/project.py CHANGED
@@ -1557,15 +1557,15 @@ class MlrunProject(ModelObj):
1557
1557
  self,
1558
1558
  item,
1559
1559
  body=None,
1560
- tag="",
1561
- local_path="",
1562
- artifact_path=None,
1563
- format=None,
1564
- upload=None,
1565
- labels=None,
1566
- target_path=None,
1560
+ tag: str = "",
1561
+ local_path: str = "",
1562
+ artifact_path: Optional[str] = None,
1563
+ format: Optional[str] = None,
1564
+ upload: Optional[bool] = None,
1565
+ labels: Optional[dict[str, str]] = None,
1566
+ target_path: Optional[str] = None,
1567
1567
  **kwargs,
1568
- ):
1568
+ ) -> Artifact:
1569
1569
  """Log an output artifact and optionally upload it to datastore
1570
1570
 
1571
1571
  If the artifact already exists with the same key and tag, it will be overwritten.
@@ -1664,7 +1664,7 @@ class MlrunProject(ModelObj):
1664
1664
  stats=None,
1665
1665
  target_path="",
1666
1666
  extra_data=None,
1667
- label_column: str = None,
1667
+ label_column: Optional[str] = None,
1668
1668
  **kwargs,
1669
1669
  ) -> DatasetArtifact:
1670
1670
  """
@@ -1741,15 +1741,15 @@ class MlrunProject(ModelObj):
1741
1741
  artifact_path=None,
1742
1742
  upload=None,
1743
1743
  labels=None,
1744
- inputs: list[Feature] = None,
1745
- outputs: list[Feature] = None,
1746
- feature_vector: str = None,
1747
- feature_weights: list = None,
1744
+ inputs: Optional[list[Feature]] = None,
1745
+ outputs: Optional[list[Feature]] = None,
1746
+ feature_vector: Optional[str] = None,
1747
+ feature_weights: Optional[list] = None,
1748
1748
  training_set=None,
1749
1749
  label_column=None,
1750
1750
  extra_data=None,
1751
1751
  **kwargs,
1752
- ):
1752
+ ) -> ModelArtifact:
1753
1753
  """Log a model artifact and optionally upload it to datastore
1754
1754
 
1755
1755
  If the model already exists with the same key and tag, it will be overwritten.
@@ -3040,8 +3040,9 @@ class MlrunProject(ModelObj):
3040
3040
  "Remote repo is not defined, use .create_remote() + push()"
3041
3041
  )
3042
3042
 
3043
- if engine not in ["remote"]:
3044
- # for remote runs we don't require the functions to be synced as they can be loaded dynamically during run
3043
+ if engine not in ["remote"] and not schedule:
3044
+ # For remote/scheduled runs we don't require the functions to be synced as they can be loaded dynamically
3045
+ # during run
3045
3046
  self.sync_functions(always=sync)
3046
3047
  if not self.spec._function_objects:
3047
3048
  raise ValueError(
@@ -578,6 +578,15 @@ class APIGateway(ModelObj):
578
578
  "true"
579
579
  )
580
580
 
581
+ def with_gateway_timeout(self, gateway_timeout: int):
582
+ """
583
+ Set gateway proxy connect/read/send timeout annotations
584
+ :param gateway_timeout: The timeout in seconds
585
+ """
586
+ mlrun.runtimes.utils.enrich_gateway_timeout_annotations(
587
+ self.metadata.annotations, gateway_timeout
588
+ )
589
+
581
590
  @classmethod
582
591
  def from_scheme(cls, api_gateway: schemas.APIGateway):
583
592
  project = api_gateway.metadata.labels.get(
@@ -15,6 +15,7 @@ import pathlib
15
15
  import typing
16
16
 
17
17
  import nuclio
18
+ import nuclio.auth
18
19
 
19
20
  import mlrun.common.schemas as schemas
20
21
  import mlrun.errors
@@ -281,34 +282,29 @@ class ApplicationRuntime(RemoteRuntime):
281
282
  is_kfp=False,
282
283
  mlrun_version_specifier=None,
283
284
  show_on_failure: bool = False,
284
- direct_port_access: bool = False,
285
- authentication_mode: schemas.APIGatewayAuthenticationMode = None,
286
- authentication_creds: tuple[str] = None,
287
- ssl_redirect: bool = None,
285
+ create_default_api_gateway: bool = True,
288
286
  ):
289
287
  """
290
288
  Deploy function, builds the application image if required (self.requires_build()) or force_build is True,
291
289
  Once the image is built, the function is deployed.
292
290
 
293
- :param project: Project name
294
- :param tag: Function tag
295
- :param verbose: Set True for verbose logging
296
- :param auth_info: Service AuthInfo (deprecated and ignored)
297
- :param builder_env: Env vars dict for source archive config/credentials
298
- e.g. builder_env={"GIT_TOKEN": token}
299
- :param force_build: Set True for force building the application image
300
- :param with_mlrun: Add the current mlrun package to the container build
301
- :param skip_deployed: Skip the build if we already have an image for the function
302
- :param is_kfp: Deploy as part of a kfp pipeline
303
- :param mlrun_version_specifier: Which mlrun package version to include (if not current)
304
- :param show_on_failure: Show logs only in case of build failure
305
- :param direct_port_access: Set True to allow direct port access to the application sidecar
306
- :param authentication_mode: API Gateway authentication mode
307
- :param authentication_creds: API Gateway authentication credentials as a tuple (username, password)
308
- :param ssl_redirect: Set True to force SSL redirect, False to disable. Defaults to
309
- mlrun.mlconf.force_api_gateway_ssl_redirect()
310
-
311
- :return: True if the function is ready (deployed)
291
+ :param project: Project name
292
+ :param tag: Function tag
293
+ :param verbose: Set True for verbose logging
294
+ :param auth_info: Service AuthInfo (deprecated and ignored)
295
+ :param builder_env: Env vars dict for source archive config/credentials
296
+ e.g. builder_env={"GIT_TOKEN": token}
297
+ :param force_build: Set True for force building the application image
298
+ :param with_mlrun: Add the current mlrun package to the container build
299
+ :param skip_deployed: Skip the build if we already have an image for the function
300
+ :param is_kfp: Deploy as part of a kfp pipeline
301
+ :param mlrun_version_specifier: Which mlrun package version to include (if not current)
302
+ :param show_on_failure: Show logs only in case of build failure
303
+ :param create_default_api_gateway: When deploy finishes the default API gateway will be created for the
304
+ application. Disabling this flag means that the application will not be
305
+ accessible until an API gateway is created for it.
306
+
307
+ :return: The default API gateway URL if created or True if the function is ready (deployed)
312
308
  """
313
309
  if (self.requires_build() and not self.spec.image) or force_build:
314
310
  self._fill_credentials()
@@ -328,10 +324,6 @@ class ApplicationRuntime(RemoteRuntime):
328
324
  self._configure_application_sidecar()
329
325
 
330
326
  # We only allow accessing the application via the API Gateway
331
- name_tag = tag or self.metadata.tag
332
- self.status.api_gateway_name = (
333
- f"{self.metadata.name}-{name_tag}" if name_tag else self.metadata.name
334
- )
335
327
  self.spec.add_templated_ingress_host_mode = (
336
328
  NuclioIngressAddTemplatedIngressModes.never
337
329
  )
@@ -344,9 +336,7 @@ class ApplicationRuntime(RemoteRuntime):
344
336
  builder_env=builder_env,
345
337
  )
346
338
  logger.info(
347
- "Successfully deployed function, creating API gateway",
348
- api_gateway_name=self.status.api_gateway_name,
349
- authentication_mode=authentication_mode,
339
+ "Successfully deployed function.",
350
340
  )
351
341
 
352
342
  # Restore the source in case it was removed to make nuclio not consider it when building
@@ -354,14 +344,23 @@ class ApplicationRuntime(RemoteRuntime):
354
344
  self.spec.build.source = self.status.application_source
355
345
  self.save(versioned=False)
356
346
 
357
- ports = self.spec.internal_application_port if direct_port_access else []
358
- return self.create_api_gateway(
359
- name=self.status.api_gateway_name,
360
- ports=ports,
361
- authentication_mode=authentication_mode,
362
- authentication_creds=authentication_creds,
363
- ssl_redirect=ssl_redirect,
364
- )
347
+ if create_default_api_gateway:
348
+ try:
349
+ api_gateway_name = self.resolve_default_api_gateway_name()
350
+ return self.create_api_gateway(api_gateway_name, set_as_default=True)
351
+ except Exception as exc:
352
+ logger.warning(
353
+ "Failed to create default API gateway, application may not be accessible. "
354
+ "Use the `create_api_gateway` method to make it accessible",
355
+ exc=mlrun.errors.err_to_str(exc),
356
+ )
357
+ elif not self.status.api_gateway:
358
+ logger.warning(
359
+ "Application is online but may not be accessible since default gateway creation was not requested."
360
+ "Use the `create_api_gateway` method to make it accessible."
361
+ )
362
+
363
+ return True
365
364
 
366
365
  def with_source_archive(
367
366
  self,
@@ -429,17 +428,54 @@ class ApplicationRuntime(RemoteRuntime):
429
428
  self,
430
429
  name: str = None,
431
430
  path: str = None,
432
- ports: list[int] = None,
431
+ direct_port_access: bool = False,
433
432
  authentication_mode: schemas.APIGatewayAuthenticationMode = None,
434
- authentication_creds: tuple[str] = None,
433
+ authentication_creds: tuple[str, str] = None,
435
434
  ssl_redirect: bool = None,
435
+ set_as_default: bool = False,
436
+ gateway_timeout: typing.Optional[int] = None,
436
437
  ):
438
+ """
439
+ Create the application API gateway. Once the application is deployed, the API gateway can be created.
440
+ An application without an API gateway is not accessible.
441
+ :param name: The name of the API gateway, defaults to <function-name>-<function-tag>
442
+ :param path: Optional path of the API gateway, default value is "/"
443
+ :param direct_port_access: Set True to allow direct port access to the application sidecar
444
+ :param authentication_mode: API Gateway authentication mode
445
+ :param authentication_creds: API Gateway basic authentication credentials as a tuple (username, password)
446
+ :param ssl_redirect: Set True to force SSL redirect, False to disable. Defaults to
447
+ mlrun.mlconf.force_api_gateway_ssl_redirect()
448
+ :param set_as_default: Set the API gateway as the default for the application (`status.api_gateway`)
449
+ :param gateway_timeout: nginx ingress timeout in sec (request timeout, when will the gateway return an
450
+ error)
451
+
452
+ :return: The API gateway URL
453
+ """
454
+ if not name:
455
+ raise mlrun.errors.MLRunInvalidArgumentError(
456
+ "API gateway name must be specified."
457
+ )
458
+
459
+ if not set_as_default and name == self.resolve_default_api_gateway_name():
460
+ raise mlrun.errors.MLRunInvalidArgumentError(
461
+ f"Non-default API gateway cannot use the default gateway name, {name=}."
462
+ )
463
+
464
+ if (
465
+ authentication_mode == schemas.APIGatewayAuthenticationMode.basic
466
+ and not authentication_creds
467
+ ):
468
+ raise mlrun.errors.MLRunInvalidArgumentError(
469
+ "Authentication credentials not provided"
470
+ )
471
+
472
+ ports = self.spec.internal_application_port if direct_port_access else []
473
+
437
474
  api_gateway = APIGateway(
438
475
  APIGatewayMetadata(
439
476
  name=name,
440
477
  namespace=self.metadata.namespace,
441
- labels=self.metadata.labels,
442
- annotations=self.metadata.annotations,
478
+ labels=self.metadata.labels.copy(),
443
479
  ),
444
480
  APIGatewaySpec(
445
481
  functions=[self],
@@ -449,13 +485,14 @@ class ApplicationRuntime(RemoteRuntime):
449
485
  ),
450
486
  )
451
487
 
488
+ api_gateway.with_gateway_timeout(gateway_timeout)
452
489
  if ssl_redirect is None:
453
490
  ssl_redirect = mlrun.mlconf.force_api_gateway_ssl_redirect()
454
491
  if ssl_redirect:
455
- # force ssl redirect so that the application is only accessible via https
492
+ # Force ssl redirect so that the application is only accessible via https
456
493
  api_gateway.with_force_ssl_redirect()
457
494
 
458
- # add authentication if required
495
+ # Add authentication if required
459
496
  authentication_mode = (
460
497
  authentication_mode
461
498
  or mlrun.mlconf.function.application.default_authentication_mode
@@ -469,18 +506,38 @@ class ApplicationRuntime(RemoteRuntime):
469
506
  api_gateway_scheme = db.store_api_gateway(
470
507
  api_gateway=api_gateway.to_scheme(), project=self.metadata.project
471
508
  )
472
- if not self.status.api_gateway_name:
473
- self.status.api_gateway_name = api_gateway_scheme.metadata.name
474
- self.status.api_gateway = APIGateway.from_scheme(api_gateway_scheme)
475
- self.status.api_gateway.wait_for_readiness()
476
- self.url = self.status.api_gateway.invoke_url
477
509
 
478
- logger.info("Successfully created API gateway", url=self.url)
479
- return self.url
510
+ if set_as_default:
511
+ self.status.api_gateway_name = api_gateway_scheme.metadata.name
512
+ self.status.api_gateway = APIGateway.from_scheme(api_gateway_scheme)
513
+ self.status.api_gateway.wait_for_readiness()
514
+ self.url = self.status.api_gateway.invoke_url
515
+ url = self.url
516
+ else:
517
+ api_gateway = APIGateway.from_scheme(api_gateway_scheme)
518
+ api_gateway.wait_for_readiness()
519
+ url = api_gateway.invoke_url
520
+ # Update application status (enriches invocation url)
521
+ self._get_state(raise_on_exception=False)
522
+
523
+ logger.info("Successfully created API gateway", url=url)
524
+ return url
525
+
526
+ def delete_api_gateway(self, name: str):
527
+ """
528
+ Delete API gateway by name.
529
+ Refreshes the application status to update api gateway and invocation URLs.
530
+ :param name: The API gateway name
531
+ """
532
+ self._get_db().delete_api_gateway(name=name, project=self.metadata.project)
533
+ if name == self.status.api_gateway_name:
534
+ self.status.api_gateway_name = None
535
+ self.status.api_gateway = None
536
+ self._get_state()
480
537
 
481
538
  def invoke(
482
539
  self,
483
- path: str,
540
+ path: str = "",
484
541
  body: typing.Optional[typing.Union[str, bytes, dict]] = None,
485
542
  method: str = None,
486
543
  headers: dict = None,
@@ -488,12 +545,25 @@ class ApplicationRuntime(RemoteRuntime):
488
545
  force_external_address: bool = False,
489
546
  auth_info: schemas.AuthInfo = None,
490
547
  mock: bool = None,
548
+ credentials: tuple[str, str] = None,
491
549
  **http_client_kwargs,
492
550
  ):
493
551
  self._sync_api_gateway()
552
+
494
553
  # If the API Gateway is not ready or not set, try to invoke the function directly (without the API Gateway)
495
554
  if not self.status.api_gateway:
496
- super().invoke(
555
+ logger.warning(
556
+ "Default API gateway is not configured, invoking function invocation URL."
557
+ )
558
+ # create a requests auth object if credentials are provided and not already set in the http client kwargs
559
+ auth = http_client_kwargs.pop("auth", None) or (
560
+ nuclio.auth.AuthInfo(
561
+ username=credentials[0], password=credentials[1]
562
+ ).to_requests_auth()
563
+ if credentials
564
+ else None
565
+ )
566
+ return super().invoke(
497
567
  path,
498
568
  body,
499
569
  method,
@@ -502,11 +572,10 @@ class ApplicationRuntime(RemoteRuntime):
502
572
  force_external_address,
503
573
  auth_info,
504
574
  mock,
575
+ auth=auth,
505
576
  **http_client_kwargs,
506
577
  )
507
578
 
508
- credentials = (auth_info.username, auth_info.password) if auth_info else None
509
-
510
579
  if not method:
511
580
  method = "POST" if body else "GET"
512
581
 
@@ -552,6 +621,13 @@ class ApplicationRuntime(RemoteRuntime):
552
621
  reverse_proxy_func.metadata.name, reverse_proxy_func.metadata.project
553
622
  )
554
623
 
624
+ def resolve_default_api_gateway_name(self):
625
+ return (
626
+ f"{self.metadata.name}-{self.metadata.tag}"
627
+ if self.metadata.tag
628
+ else self.metadata.name
629
+ )
630
+
555
631
  @min_nuclio_versions("1.13.1")
556
632
  def disable_default_http_trigger(
557
633
  self,