orient_express 0.4.2__tar.gz → 2.0__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.
@@ -0,0 +1,670 @@
1
+ Metadata-Version: 2.4
2
+ Name: orient_express
3
+ Version: 2.0
4
+ Summary: A library to simplify model deployment to Vertex AI
5
+ Author: Alexey Zankevich
6
+ Author-email: alex.zankevich@shiftsmart.com
7
+ Requires-Python: >=3.10,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: google-cloud-aiplatform
13
+ Requires-Dist: google-cloud-storage
14
+ Requires-Dist: pandas
15
+ Requires-Dist: pyyaml (>=6.0,<7.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Orient Express
19
+
20
+ A library to accelerate model deployments to Vertex AI directly from colab notebooks
21
+
22
+ ![train-resized](https://github.com/user-attachments/assets/f1ed32ec-07d9-4d48-8b96-3323db6b5091)
23
+
24
+ Orient Express provides two main capabilities:
25
+
26
+ 1. **Vertex Model Deployment and Retrieval**: Capabilities for uploading, downloading, or deploying models to Vertex AI Model Registry.
27
+
28
+ 1. **ONNX Image Model Deployment**: Built-in predictor classes for easily running image classification, object detection, instance segmentation, and semantic segmentation models exported to ONNX format.
29
+
30
+ Both workflows handle versioning, artifact storage in GCS, and integration with Vertex AI Model Registry.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install orient_express
36
+ ```
37
+
38
+ ## Workflows
39
+
40
+ ### ONNX Image Model Workflow
41
+
42
+ This workflow is for deploying image models (classification, detection, segmentation) exported to ONNX format.
43
+
44
+ ```python
45
+ from orient_express.predictors import ClassificationPredictor
46
+ from orient_express.vertex import upload_model, get_vertex_model
47
+
48
+ # 1. Create predictor from your exported ONNX model
49
+ predictor = ClassificationPredictor(
50
+ onnx_path="model.onnx",
51
+ classes={1: "cat", 2: "dog", 3: "bird"}
52
+ )
53
+
54
+ # 2. Upload to Vertex AI Model Registry
55
+ vertex_model = upload_model(
56
+ model=predictor,
57
+ model_name="my-classifier",
58
+ project_name="my-project",
59
+ region="us-central1",
60
+ bucket_name="my-artifacts-bucket",
61
+ )
62
+
63
+ # 3. Later, retrieve and run locally
64
+ vertex_model = get_vertex_model(
65
+ model_name="my-classifier",
66
+ project_name="my-project",
67
+ region="us-central1",
68
+ )
69
+ local_predictor = vertex_model.get_local_predictor()
70
+
71
+ from PIL import Image
72
+ images = [Image.open("test.jpg")]
73
+ predictions = local_predictor.predict(images)
74
+
75
+ # 4. Or deploy to an endpoint for remote inference
76
+ vertex_model.deploy_to_endpoint(
77
+ endpoint_name="my-classifier-endpoint",
78
+ machine_type="n1-standard-4",
79
+ min_replica_count=1,
80
+ max_replica_count=3,
81
+ )
82
+
83
+ # remote prediction API depends on the endpoint container deployed with the model
84
+ predictions = vertex_model.remote_predict(
85
+ [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
86
+ endpoint_name="my-classifier-endpoint"
87
+ )
88
+ ```
89
+
90
+ ### Joblib Model Workflow
91
+
92
+ This workflow is for deploying models that can be serialized with joblib, such as scikit-learn pipelines or XGBoost models.
93
+
94
+ ```python
95
+ from sklearn.pipeline import Pipeline
96
+ from sklearn.compose import ColumnTransformer
97
+ from sklearn.preprocessing import StandardScaler, OneHotEncoder
98
+ from sklearn.impute import SimpleImputer
99
+ import xgboost as xgb
100
+ import seaborn as sns
101
+
102
+ from orient_express.vertex import upload_model_joblib, get_vertex_model
103
+
104
+ # 1. Train your model
105
+ data = sns.load_dataset('titanic').dropna(subset=['survived'])
106
+ X = data[['pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked']]
107
+ y = data['survived']
108
+
109
+ numeric_features = ['age', 'fare', 'sibsp', 'parch']
110
+ numeric_transformer = Pipeline(steps=[
111
+ ('imputer', SimpleImputer(strategy='median')),
112
+ ('scaler', StandardScaler())
113
+ ])
114
+
115
+ categorical_features = ['pclass', 'sex', 'embarked']
116
+ categorical_transformer = Pipeline(steps=[
117
+ ('imputer', SimpleImputer(strategy='most_frequent')),
118
+ ('onehot', OneHotEncoder(handle_unknown='ignore'))
119
+ ])
120
+
121
+ preprocessor = ColumnTransformer(transformers=[
122
+ ('num', numeric_transformer, numeric_features),
123
+ ('cat', categorical_transformer, categorical_features)
124
+ ])
125
+
126
+ model = Pipeline(steps=[
127
+ ('preprocessor', preprocessor),
128
+ ('classifier', xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss'))
129
+ ])
130
+
131
+ model.fit(X, y)
132
+
133
+ # 2. Upload to Vertex AI Model Registry
134
+ vertex_model = upload_model_joblib(
135
+ model=model,
136
+ model_name="titanic-classifier",
137
+ project_name="my-project",
138
+ region="us-central1",
139
+ bucket_name="my-artifacts-bucket",
140
+ serving_container_image_uri="your-serving-container:latest",
141
+ serving_container_health_route="/health",
142
+ serving_container_predict_route="/predict",
143
+ )
144
+
145
+ # 3. Later, retrieve the model
146
+ vertex_model = get_vertex_model(
147
+ model_name="titanic-classifier",
148
+ project_name="my-project",
149
+ region="us-central1",
150
+ )
151
+
152
+ # 4. Run locally
153
+ local_predictor = vertex_model.get_local_predictor()
154
+ predictions = local_predictor.predict(X_test)
155
+ ```
156
+
157
+ ### Pinning Model Versions
158
+
159
+ By default, `get_vertex_model` returns the most recently updated version. To pin to a specific version:
160
+
161
+ ```python
162
+ vertex_model = get_vertex_model(
163
+ model_name="my-classifier",
164
+ project_name="my-project",
165
+ region="us-central1",
166
+ version=3, # Pin to version 3
167
+ )
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Built-in Predictor Types
173
+
174
+ Orient Express provides four built-in predictor classes for ONNX image models. Each has specific requirements for the ONNX graph structure.
175
+
176
+ ### General ONNX Requirements
177
+
178
+ All ONNX image models share these requirements:
179
+
180
+ - **Input images are resized using simple stretch** (no letterboxing/padding) to the model's expected resolution before inference.
181
+ - **Normalization must be baked into the ONNX graph.** The library passes uint8 RGB images directly to the model; any normalization (e.g., ImageNet mean/std) must be handled inside the graph.
182
+ - **Batch dimension**: Models receive batched inputs with shape `[batch, height, width, 3]`.
183
+
184
+ ### ClassificationPredictor
185
+
186
+ <details>
187
+ <summary>Click to expand</summary>
188
+
189
+ For image classification models that output class probabilities.
190
+
191
+ #### ONNX Graph Requirements
192
+
193
+ | | |
194
+ | ----------- | ------------------------------------------------------------ |
195
+ | **Inputs** | `images`: `[batch, height, width, 3]` uint8 RGB |
196
+ | **Outputs** | `scores`: `[batch, num_classes]` float32 class probabilities |
197
+
198
+ The graph must handle normalization internally. No target_sizes input is needed.
199
+
200
+ #### Usage
201
+
202
+ ```python
203
+ from orient_express.predictors import ClassificationPredictor
204
+
205
+ predictor = ClassificationPredictor(
206
+ onnx_path="classifier.onnx",
207
+ classes={1: "cat", 2: "dog", 3: "bird"}
208
+ )
209
+
210
+ predictions = predictor.predict(images)
211
+ # Returns: list[ClassificationPrediction]
212
+ ```
213
+
214
+ #### Output Structure
215
+
216
+ ```python
217
+ @dataclass
218
+ class ClassificationPrediction:
219
+ clss: str # Predicted class name
220
+ score: float # Confidence score for predicted class
221
+ class_scores: dict[str, float] # Scores for all classes
222
+
223
+ # to_dict() output:
224
+ {
225
+ "class": "cat",
226
+ "score": 0.95,
227
+ "class_scores": {"cat": 0.95, "dog": 0.03, "bird": 0.02}
228
+ }
229
+ ```
230
+
231
+ </details>
232
+
233
+ ### BoundingBoxPredictor
234
+
235
+ <details>
236
+ <summary>Click to expand</summary>
237
+
238
+ For object detection models that output bounding boxes.
239
+
240
+ #### ONNX Graph Requirements
241
+
242
+ | | |
243
+ | ----------- | ------------------------------------------------------------------------------------------------- |
244
+ | **Inputs** | `images`: `[batch, height, width, 3]` uint8 RGB |
245
+ | | `target_sizes`: `[batch, 2]` float32 containing `[height, width]` of original images |
246
+ | **Outputs** | `boxes`: `[batch, num_detections, 4]` float32 as `[x1, y1, x2, y2]` in original image coordinates |
247
+ | | `scores`: `[batch, num_detections]` float32 confidence scores |
248
+ | | `labels`: `[batch, num_detections]` int64 class indices |
249
+
250
+ The ONNX graph must rescale bounding boxes to the original image dimensions using `target_sizes`. The library does not perform any box coordinate transformation.
251
+
252
+ #### Usage
253
+
254
+ ```python
255
+ from orient_express.predictors import BoundingBoxPredictor
256
+
257
+ predictor = BoundingBoxPredictor(
258
+ onnx_path="detector.onnx",
259
+ classes={1: "person", 2: "car", 3: "bicycle"}
260
+ )
261
+
262
+ predictions = predictor.predict(images, confidence=0.5, nms_threshold=0.4)
263
+ # Returns: list[list[BoundingBoxPrediction]]
264
+ # Outer list: per image, inner list: detections for that image
265
+ ```
266
+
267
+ #### Output Structure
268
+
269
+ ```python
270
+ @dataclass
271
+ class BoundingBoxPrediction:
272
+ clss: str # Class name
273
+ score: float # Confidence score
274
+ bbox: np.ndarray # [x1, y1, x2, y2] in original image coordinates
275
+
276
+ # to_dict() output:
277
+ {
278
+ "class": "person",
279
+ "score": 0.92,
280
+ "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}
281
+ }
282
+ ```
283
+
284
+ #### Annotation
285
+
286
+ ```python
287
+ annotated_image = predictor.get_annotated_image(image, predictions[0])
288
+ # Returns PIL.Image with bounding boxes drawn
289
+ ```
290
+
291
+ </details>
292
+
293
+ ### InstanceSegmentationPredictor
294
+
295
+ <details>
296
+ <summary>Click to expand</summary>
297
+
298
+ For instance segmentation models that output bounding boxes and per-instance masks.
299
+
300
+ #### ONNX Graph Requirements
301
+
302
+ | | |
303
+ | ----------- | ------------------------------------------------------------------------------------------------- |
304
+ | **Inputs** | `images`: `[batch, height, width, 3]` uint8 RGB |
305
+ | | `target_sizes`: `[batch, 2]` float32 containing `[height, width]` of original images |
306
+ | **Outputs** | `boxes`: `[batch, num_detections, 4]` float32 as `[x1, y1, x2, y2]` in original image coordinates |
307
+ | | `scores`: `[batch, num_detections]` float32 confidence scores |
308
+ | | `labels`: `[batch, num_detections]` int64 class indices |
309
+ | | `masks`: `[batch, num_detections, mask_height, mask_width]` float32 mask logits |
310
+
311
+ The ONNX graph must rescale bounding boxes to original image dimensions using `target_sizes`. Masks can be any resolution—they are resized to original image dimensions in Python postprocessing using bilinear interpolation.
312
+
313
+ #### Usage
314
+
315
+ ```python
316
+ from orient_express.predictors import InstanceSegmentationPredictor
317
+
318
+ predictor = InstanceSegmentationPredictor(
319
+ onnx_path="instance_seg.onnx",
320
+ classes={1: "person", 2: "car", 3: "bicycle"}
321
+ )
322
+
323
+ predictions = predictor.predict(images, confidence=0.5)
324
+ # Returns: list[list[InstanceSegmentationPrediction]]
325
+ ```
326
+
327
+ #### Output Structure
328
+
329
+ ```python
330
+ @dataclass
331
+ class InstanceSegmentationPrediction:
332
+ clss: str # Class name
333
+ score: float # Confidence score
334
+ bbox: np.ndarray # [x1, y1, x2, y2] in original image coordinates
335
+ mask: np.ndarray # Boolean mask at original image resolution
336
+
337
+ # to_dict(include_mask=False) output:
338
+ {
339
+ "class": "person",
340
+ "score": 0.89,
341
+ "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}
342
+ }
343
+
344
+ # to_dict(include_mask=True) adds:
345
+ {
346
+ ...
347
+ "mask": [[True, True, False, ...], ...] # 2D boolean list
348
+ }
349
+ ```
350
+
351
+ #### Annotation
352
+
353
+ ```python
354
+ annotated_image = predictor.get_annotated_image(image, predictions[0])
355
+ # Returns PIL.Image with mask overlays and instance indices
356
+ ```
357
+
358
+ </details>
359
+
360
+ ### SemanticSegmentationPredictor
361
+
362
+ <details>
363
+ <summary>Click to expand</summary>
364
+
365
+ For semantic segmentation models that output per-pixel class predictions.
366
+
367
+ #### ONNX Graph Requirements
368
+
369
+ | | |
370
+ | ----------- | ----------------------------------------------------------------------------- |
371
+ | **Inputs** | `images`: `[batch, height, width, 3]` uint8 RGB |
372
+ | **Outputs** | `masks`: `[batch, num_classes, mask_height, mask_width]` float32 class logits |
373
+
374
+ Masks can be any resolution—they are resized to original image dimensions in Python postprocessing. The class dimension is reduced via argmax to produce a single class ID per pixel.
375
+
376
+ #### Usage
377
+
378
+ ```python
379
+ from orient_express.predictors import SemanticSegmentationPredictor
380
+
381
+ predictor = SemanticSegmentationPredictor(
382
+ onnx_path="semantic_seg.onnx",
383
+ classes={0: "background", 1: "road", 2: "building", 3: "vegetation"}
384
+ )
385
+
386
+ predictions = predictor.predict(images)
387
+ # Returns: list[SemanticSegmentationPrediction]
388
+ ```
389
+
390
+ #### Output Structure
391
+
392
+ ```python
393
+ @dataclass
394
+ class SemanticSegmentationPrediction:
395
+ class_mask: np.ndarray # [height, width] int array of class indices
396
+ conf_masks: np.ndarray # [num_classes, height, width] float confidence per class
397
+
398
+ # to_dict(include_conf_masks=False) output:
399
+ {
400
+ "class_mask": [[0, 0, 1, 2, ...], ...] # 2D int array
401
+ }
402
+
403
+ # to_dict(include_conf_masks=True) adds:
404
+ {
405
+ ...
406
+ "conf_masks": [[[0.1, 0.2, ...], ...], ...] # 3D float array
407
+ }
408
+ ```
409
+
410
+ #### Annotation
411
+
412
+ ```python
413
+ annotated_image = predictor.get_annotated_image(image, predictions[0].class_mask)
414
+ # Returns PIL.Image with color-coded segmentation overlay
415
+ ```
416
+
417
+ </details>
418
+
419
+ ---
420
+
421
+ ## Color Schemes
422
+
423
+ For predictors that support annotation (`BoundingBoxPredictor`, `InstanceSegmentationPredictor`, `SemanticSegmentationPredictor`), you can set a custom color scheme:
424
+
425
+ ```python
426
+ predictor.color_scheme = {
427
+ "person": (255, 0, 0), # Red (RGB)
428
+ "car": (0, 255, 0), # Green
429
+ "bicycle": (0, 0, 255), # Blue
430
+ }
431
+ ```
432
+
433
+ Colors are specified as RGB tuples.
434
+
435
+ ## Legacy API [Still Maintained]
436
+
437
+ <details>
438
+ <summary>Click to expand</summary>
439
+
440
+ ## Example
441
+
442
+ ### Train Model
443
+
444
+ Train a regular model. In the example below, it's xgboost model, trained on the Titanic dataset.
445
+
446
+ ```python
447
+
448
+ # Import necessary libraries
449
+ import xgboost as xgb
450
+ from sklearn.model_selection import train_test_split
451
+ from sklearn.metrics import accuracy_score, confusion_matrix
452
+ from sklearn.pipeline import Pipeline
453
+ from sklearn.compose import ColumnTransformer
454
+ from sklearn.preprocessing import StandardScaler, OneHotEncoder
455
+ from sklearn.impute import SimpleImputer
456
+
457
+ # Load the Titanic dataset
458
+ data = sns.load_dataset('titanic').dropna(subset=['survived']) # Dropping rows with missing target labels
459
+
460
+ # Select features and target
461
+ X = data[['pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked']]
462
+ y = data['survived']
463
+
464
+ # Define preprocessing for numeric columns (impute missing values and scale features)
465
+ numeric_features = ['age', 'fare', 'sibsp', 'parch']
466
+ numeric_transformer = Pipeline(steps=[
467
+ ('imputer', SimpleImputer(strategy='median')),
468
+ ('scaler', StandardScaler())
469
+ ])
470
+
471
+ # Define preprocessing for categorical columns (impute missing values and one-hot encode)
472
+ categorical_features = ['pclass', 'sex', 'embarked']
473
+ categorical_transformer = Pipeline(steps=[
474
+ ('imputer', SimpleImputer(strategy='most_frequent')),
475
+ ('onehot', OneHotEncoder(handle_unknown='ignore'))
476
+ ])
477
+
478
+ # Combine preprocessing steps
479
+ preprocessor = ColumnTransformer(
480
+ transformers=[
481
+ ('num', numeric_transformer, numeric_features),
482
+ ('cat', categorical_transformer, categorical_features)
483
+ ])
484
+
485
+ # Create a pipeline that first transforms the data, then trains an XGBoost model
486
+ model = Pipeline(steps=[
487
+ ('preprocessor', preprocessor),
488
+ ('classifier', xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss'))
489
+ ])
490
+
491
+ # Split the dataset into training and test sets
492
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
493
+
494
+ # Train the model
495
+ model.fit(X_train, y_train)
496
+ ```
497
+
498
+ ## Upload Model To Model Registry
499
+
500
+ ```python
501
+
502
+ model_wrapper = ModelExpress(model=model,
503
+ project_name='my-project-name',
504
+ region='us-central1',
505
+ bucket_name='my-artifacts-bucket',
506
+ model_name='titanic')
507
+ model_wrapper.upload()
508
+ ```
509
+
510
+ ## Local Inference (Without Online Prediction Endpoint)
511
+
512
+ The following code will download the last model from the model registry and run the inference locally.
513
+
514
+ ```python
515
+
516
+ # create input dataframe
517
+ titanic_data = {
518
+ "pclass": [1], # Passenger class (1st, 2nd, 3rd)
519
+ "sex": ["female"], # Gender
520
+ "age": [29], # Age
521
+ "sibsp": [0], # Number of siblings/spouses aboard
522
+ "parch": [0], # Number of parents/children aboard
523
+ "fare": [100.0], # Ticket fare
524
+ "embarked": ["S"] # Port of Embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
525
+ }
526
+ input_df = pd.DataFrame(titanic_data)
527
+
528
+ # init the model wrapper
529
+ model_wrapper = ModelExpress(project_name='my-project-name',
530
+ region='us-central1',
531
+ model_name='titanic')
532
+
533
+ # Run inference locally
534
+ # It will download the most recent version from the model registry automatically
535
+ model_wrapper.local_predict(input_df)
536
+ ```
537
+
538
+ ## Pin Model Version
539
+
540
+ In many cases, the pipeline should be pinned to a specific model version so the model can only
541
+ be updated explicitly. Just pass a `model_version` parameter when instantiating the ModelExpress wrapper.
542
+
543
+ ```python
544
+
545
+ # init the model wrapper
546
+ model_wrapper = ModelExpress(project_name='my-project-name',
547
+ region='us-central1',
548
+ model_name='titanic',
549
+ model_version=11)
550
+ ```
551
+
552
+ ## Remote Inference (With Online Prediction Endpoint)
553
+
554
+ Make sure the model is deployed:
555
+
556
+ ```python
557
+
558
+ model_wrapper = ModelExpress(model=model,
559
+ project_name='my-project-name',
560
+ region='us-central1',
561
+ bucket_name='my-artifacts-bucket',
562
+ model_name='titanic')
563
+
564
+ # upload the version to the registry and deploy it to the endpoint
565
+ model_wrapper.deploy()
566
+ ```
567
+
568
+ Run inference with `remote_predict` method. It will make a remote call to the endpoint without fetching the model locally.
569
+
570
+ ```python
571
+
572
+ titanic_data = {
573
+ "pclass": [1], # Passenger class (1st, 2nd, 3rd)
574
+ "sex": ["female"], # Gender
575
+ "age": [29], # Age
576
+ "sibsp": [0], # Number of siblings/spouses aboard
577
+ "parch": [0], # Number of parents/children aboard
578
+ "fare": [100.0], # Ticket fare
579
+ "embarked": ["S"] # Port of Embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
580
+ }
581
+ df = pd.DataFrame(titanic_data)
582
+
583
+ model_wrapper.remote_predict(df)
584
+ ```
585
+
586
+ ## Pipeline Deployment Function
587
+
588
+ Orient express library also have a helper function to simplify Vertex AI pipeline deployment.
589
+
590
+ Create `deploy.py` script
591
+
592
+ ```python
593
+
594
+ from orient_express.deployment import deploy_pipeline
595
+
596
+ import argparse
597
+ import conf
598
+
599
+ from pipeline import pipeline
600
+ from orient_express.deployment import deploy_pipeline
601
+
602
+
603
+ if __name__ == "__main__":
604
+ parser = argparse.ArgumentParser()
605
+ parser.add_argument("--run-type", required=True)
606
+
607
+ args = parser.parse_args()
608
+ deploy_pipeline(run_type=args.run_type,
609
+ pipeline_dsl=pipeline,
610
+ pipeline_root=conf.PIPELINE_ROOT,
611
+ pipeline_name=conf.PIPELINE_NAME,
612
+ pipeline_display_name=conf.PIPELINE_DISPLAY_NAME,
613
+ pipeline_schedule_name=conf.SCHEDULE_NAME,
614
+ gcp_project=conf.PROJECT_ID,
615
+ gcp_location='us-central1',
616
+ gcp_service_account=conf.SERVICE_ACCOUNT,
617
+ gcp_network=conf.NETWORK_NAME,
618
+ gcp_labels={"team": "ml"})
619
+ ```
620
+
621
+ And conf.py, make sure to replace the sample values with yours.
622
+
623
+ ```python
624
+
625
+ import os
626
+
627
+ BASE_PATH = "gs://pipelines-bucket/vertex-ai/pipelines"
628
+
629
+ PIPELINE_NAME = "my-pipeline"
630
+ PIPELINE_ROOT = f"{BASE_PATH}/{PIPELINE_NAME}"
631
+ PIPELINE_TEMP_ROOT = f"{BASE_PATH}/{PIPELINE_NAME}-temp"
632
+
633
+ PIPELINE_DISPLAY_NAME = "My Pipeline"
634
+ PIPELINE_DESCRIPTION = "My example pipeline"
635
+
636
+ NETWORK_NAME = "project network id"
637
+
638
+ DOCKER_IMAGE = "us-docker.pkg.dev/my-project/my-artifactory/my-pipeline:latest
639
+ BASE_IMAGE = "python:3.11"
640
+ PROJECT_ID = "my-project"
641
+ PROJECT_REGION = "us-central1"
642
+
643
+ SERVICE_ACCOUNT = "my-service-account@my-project.iam.gserviceaccount.com"
644
+ SCHEDULE_NAME = "My Pipeline"
645
+ ```
646
+
647
+ For testing it on a local machine, make sure to authorize to GCP first
648
+
649
+ ```shell
650
+
651
+ gcloud auth application-default login
652
+
653
+ ```
654
+
655
+ Finally, run the pipeline (it will execute once)
656
+
657
+ ```shell
658
+
659
+ python deploy.py --run-type single-run
660
+ ```
661
+
662
+ Or, create a scheduler to run continuously
663
+
664
+ ```shell
665
+
666
+ python deploy.py --run-type scheduled
667
+ ```
668
+
669
+ </details>
670
+