29
29
import tarfile
30
30
import tempfile
31
31
import time
32
+ import uuid
32
33
from collections .abc import Callable
33
34
from dataclasses import dataclass
34
35
from datetime import datetime
@@ -302,6 +303,15 @@ class Application:
302
303
experiments_endpoint : str = "{base}/experiments"
303
304
"""Base endpoint for the experiments in the application."""
304
305
306
+ # Local experience parameters.
307
+ src : Optional [str ] = None
308
+ """
309
+ Source of the application, if initialized locally. This is the path
310
+ to the application's source code.
311
+ """
312
+ description : Optional [str ] = None
313
+ """Description of the application."""
314
+
305
315
def __post_init__ (self ):
306
316
"""Initialize the endpoint and experiments_endpoint attributes.
307
317
@@ -311,6 +321,158 @@ def __post_init__(self):
311
321
self .endpoint = self .endpoint .format (id = self .id )
312
322
self .experiments_endpoint = self .experiments_endpoint .format (base = self .endpoint )
313
323
324
+ @classmethod
325
+ def initialize (
326
+ cls ,
327
+ name : str ,
328
+ id : Optional [str ] = None ,
329
+ description : Optional [str ] = None ,
330
+ destination : Optional [str ] = None ,
331
+ client : Optional [Client ] = None ,
332
+ ) -> "Application" :
333
+ """
334
+ Initialize a Nextmv application, locally.
335
+
336
+ This method will create a new application in the local file system. The
337
+ application is a folder with the name given by `name`, under the
338
+ location given by `destination`. If the `destination` parameter is not
339
+ specified, the current working directory is used as default. This
340
+ method will scaffold the application with the necessary files and
341
+ directories to have an opinionated structure for your decision model.
342
+ Once the application is initialized, you are encouraged to complete it
343
+ with the decision model itself, so that the application can be run,
344
+ locally or remotely.
345
+
346
+ This method differs from the `Application.new` method in that it
347
+ creates the application locally rather than in the Cloud.
348
+
349
+ Although not required, you are encouraged to specify the `client`
350
+ parameter, so that the application can be pushed and synced remotely,
351
+ with the Nextmv Cloud. If you don't specify the `client`, and intend to
352
+ interact with the Nextmv Cloud, you will encounter an error. Make sure
353
+ you set the `client` parameter on the `Application` instance after
354
+ initialization, if you don't provide it here.
355
+
356
+ Use the `destination` parameter to specify where you want the app to be
357
+ initialized, using the current working directory by default.
358
+
359
+ Parameters
360
+ ----------
361
+ name : str
362
+ Name of the application.
363
+ id : str, optional
364
+ ID of the application. Will be generated if not provided.
365
+ description : str, optional
366
+ Description of the application.
367
+ destination : str, optional
368
+ Destination directory where the application will be initialized. If
369
+ not provided, the current working directory will be used.
370
+ client : Client, optional
371
+ Client to use for interacting with the Nextmv Cloud API.
372
+
373
+ Returns
374
+ -------
375
+ Application
376
+ The initialized application instance.
377
+ """
378
+
379
+ destination_dir = os .getcwd () if destination is None else destination
380
+ app_id = id if id is not None else str (uuid .uuid4 ())
381
+
382
+ # Create the new directory with the given name.
383
+ src = os .path .join (destination_dir , name )
384
+ os .makedirs (src , exist_ok = True )
385
+
386
+ # Get the path to the initial app structure template.
387
+ current_file_dir = os .path .dirname (os .path .abspath (__file__ ))
388
+ initial_app_structure_path = os .path .join (current_file_dir , ".." , "default_app" )
389
+ initial_app_structure_path = os .path .normpath (initial_app_structure_path )
390
+
391
+ # Copy everything from initial_app_structure to the new directory.
392
+ if os .path .exists (initial_app_structure_path ):
393
+ for item in os .listdir (initial_app_structure_path ):
394
+ source_path = os .path .join (initial_app_structure_path , item )
395
+ dest_path = os .path .join (src , item )
396
+
397
+ if os .path .isdir (source_path ):
398
+ shutil .copytree (source_path , dest_path , dirs_exist_ok = True )
399
+ continue
400
+
401
+ shutil .copy2 (source_path , dest_path )
402
+
403
+ return cls (
404
+ id = app_id ,
405
+ client = client ,
406
+ src = src ,
407
+ description = description ,
408
+ )
409
+
410
+ @classmethod
411
+ def new (
412
+ cls ,
413
+ client : Client ,
414
+ name : str ,
415
+ id : Optional [str ] = None ,
416
+ description : Optional [str ] = None ,
417
+ is_workflow : Optional [bool ] = None ,
418
+ exist_ok : bool = False ,
419
+ ) -> "Application" :
420
+ """
421
+ Create a new application directly in Nextmv Cloud.
422
+
423
+ The application is created as an empty shell, and executable code must
424
+ be pushed to the app before running it remotely.
425
+
426
+ Parameters
427
+ ----------
428
+ client : Client
429
+ Client to use for interacting with the Nextmv Cloud API.
430
+ name : str
431
+ Name of the application.
432
+ id : str, optional
433
+ ID of the application. Will be generated if not provided.
434
+ description : str, optional
435
+ Description of the application.
436
+ is_workflow : bool, optional
437
+ Whether the application is a Decision Workflow.
438
+ exist_ok : bool, default=False
439
+ If True and an application with the same ID already exists,
440
+ return the existing application instead of creating a new one.
441
+
442
+ Returns
443
+ -------
444
+ Application
445
+ The newly created (or existing) application.
446
+
447
+ Examples
448
+ --------
449
+ >>> from nextmv.cloud import Client
450
+ >>> client = Client(api_key="your-api-key")
451
+ >>> app = Application.new(client=client, name="My New App", id="my-app")
452
+ """
453
+
454
+ if exist_ok and cls .exists (client = client , id = id ):
455
+ return Application (client = client , id = id )
456
+
457
+ payload = {
458
+ "name" : name ,
459
+ }
460
+
461
+ if description is not None :
462
+ payload ["description" ] = description
463
+ if id is not None :
464
+ payload ["id" ] = id
465
+ if is_workflow is not None :
466
+ payload ["is_pipeline" ] = is_workflow
467
+
468
+ response = client .request (
469
+ method = "POST" ,
470
+ endpoint = "v1/applications" ,
471
+ payload = payload ,
472
+ )
473
+
474
+ return cls (client = client , id = response .json ()["id" ])
475
+
314
476
def acceptance_test (self , acceptance_test_id : str ) -> AcceptanceTest :
315
477
"""
316
478
Retrieve details of an acceptance test.
@@ -902,69 +1064,6 @@ def managed_input(self, managed_input_id: str) -> ManagedInput:
902
1064
903
1065
return ManagedInput .from_dict (response .json ())
904
1066
905
- @classmethod
906
- def new (
907
- cls ,
908
- client : Client ,
909
- name : str ,
910
- id : Optional [str ] = None ,
911
- description : Optional [str ] = None ,
912
- is_workflow : Optional [bool ] = None ,
913
- exist_ok : bool = False ,
914
- ) -> "Application" :
915
- """
916
- Create a new application.
917
-
918
- Parameters
919
- ----------
920
- client : Client
921
- Client to use for interacting with the Nextmv Cloud API.
922
- name : str
923
- Name of the application.
924
- id : str, optional
925
- ID of the application. Will be generated if not provided.
926
- description : str, optional
927
- Description of the application.
928
- is_workflow : bool, optional
929
- Whether the application is a Decision Workflow.
930
- exist_ok : bool, default=False
931
- If True and an application with the same ID already exists,
932
- return the existing application instead of creating a new one.
933
-
934
- Returns
935
- -------
936
- Application
937
- The newly created (or existing) application.
938
-
939
- Examples
940
- --------
941
- >>> from nextmv.cloud import Client
942
- >>> client = Client(api_key="your-api-key")
943
- >>> app = Application.new(client=client, name="My New App", id="my-app")
944
- """
945
-
946
- if exist_ok and cls .exists (client = client , id = id ):
947
- return Application (client = client , id = id )
948
-
949
- payload = {
950
- "name" : name ,
951
- }
952
-
953
- if description is not None :
954
- payload ["description" ] = description
955
- if id is not None :
956
- payload ["id" ] = id
957
- if is_workflow is not None :
958
- payload ["is_pipeline" ] = is_workflow
959
-
960
- response = client .request (
961
- method = "POST" ,
962
- endpoint = "v1/applications" ,
963
- payload = payload ,
964
- )
965
-
966
- return cls (client = client , id = response .json ()["id" ])
967
-
968
1067
def new_acceptance_test (
969
1068
self ,
970
1069
candidate_instance_id : str ,
0 commit comments