Projects#

Projects are the primary way to organize Tasks in the Inductiva API. A project is a collection of tasks that run under a common umbrella, identified by its name. Any submitted task will always belong to a project: either a user-specified one or the default project. This allows users to organize their tasks in a way that makes sense to them. The following sections will explain how to create and interact with projects.

Creating a Project#

A project is created whenever an instance of the Project class, from the projects module, is created. Projects are uniquely identified by their name.

When instantiating a new project, the user needs to specify the project’s name - if one with the same name already exists, a reference to that project will be returned, otherwise, a new one is created. The following snippet creates an empty project called my_project and prints a description of it:

>>> import inductiva
>>> project = inductiva.projects.Project("my_project")
>>> print(project.describe())
Project 'my_project' with 0 tasks (id=35ea9cf0-e8ce-47cf-b6cf-6a3eb4994d98)

# At this point, any new Project instantiated with the same name will
# refer to the same project, even though the object is different:
>>> project2 = inductiva.projects.Project("my_project")
>>> print(project2 is project) # test if the objects are the same
False
>>> print(project2 == project) # test if the projects are the same
True

NOTE: A default project is automatically created when a new user account is added. Typically, the default project is named after the user’s username with some random characters appended to it. This project is used when no project is explicitly used in the task submission and ensures that no orphaned tasks are created.

Listing existing projects#

To retrieve a list of all existing projects, use the get_projects function from the projects module:

>>> import inductiva
>>> projects = inductiva.projects.get_projects()
[<inductiva.projects.project.Project at 0x123f22110>,
 ...
 <inductiva.projects.project.Project at 0x123f00190>]

Alternatively, you can use the inductiva projects list CLI command to list all existing projects:

$ inductiva projects list

 NAME           NR_TASKS
 my_project     0
 ...
 userab1cdef2   241     # default project

Adding a Task to a Project#

Any task that is submitted to the Inductiva API must belong to a project. When the user is not explicit about the project to which the task is to be added, the default one is used. This ensures that no orphaned tasks are created. To add a task to a project other than the default one, the user needs to create a new project (or reference an existing one) and open it for task submission. However, the project can only be opened for task submission if it gets instantiated with an explicit append=True argument in the constructor or, otherwise, a RunTime error will be raised. This mechanism is meant to work as a security measure that ensures that the user is explicitly aware that new tasks are being appended to the project, which is particularly useful to ensure the consistency of the project’s content.

>>> import inductiva
>>> project = inductiva.projects.Project("demo", append=False)
>>> project.open() # <-- An exception will be raised because the project is appendable
...
RuntimeError: Trying to open a project with `append=False`.
A Project can only be opened when instantiated with the `append=True` option.

Whenever the run method of a Simulator object is called, the resulting task will be created under the project that is currently open, even though the project is not explicitly specified as an argument to the run method. It will be inferred from the context where the run method is being called.

The client library offers two mechanisms to manage how tasks are added to a project: by manually opening and closing a project or via a context manager.

Explicit management#

Explicit management requires that the user opens and closes the project manually using the open and close methods of the Project class. The project needs to be instantiated, opened for task submission and then closed to prevent further tasks from being appended to it. Any task submitted between the opening and closing of the project will be added to it. The following snippet demonstrates how to add a task to a project using explicit management:

import inductiva

# get example input data
input_dir = inductiva.utils.download_from_url(
    "https://storage.googleapis.com/inductiva-api-demo-files/"
    "xbeach-input-example.zip", unzip=True)

project = inductiva.projects.Project("my_xbeach_project", append=True)

project.open() # <-- open the project for task submission

simulator = inductiva.simulators.XBeach()

# add a task to the "my_xbeach_project" project
task1 = simulator.run(input_dir=input_dir,
                      sim_config_filename="params.txt")

project.close() # <-- close the project

# task2 will be added to the default project
task2 = simulator.run(input_dir=input_dir,
                      sim_config_filename="params.txt")

print(task1.get_info().project) # "my_xbeach_project"
print(task2.get_info().project) # "userab1cdef2" (default project)

Using a Context Manager#

Alternatively, the project can be managed using a context manager that automatically opens and closes the project for task submission, and helps clarify the scope of the project. The manager ensures that the project is opened for task submission by calling the open and close methods when entering and exiting the context of the with block, respectively. Any task that is created inside the with block will be appended to the project managed in that context; tasks submitted outside will be added to the default one:

import inductiva

# get example input data
input_dir = inductiva.utils.download_from_url(
    "https://storage.googleapis.com/inductiva-api-demo-files/"
    "xbeach-input-example.zip", unzip=True)

with inductiva.projects.Project("my_xbeach_project", append=True) as project:
    simulator = inductiva.simulators.XBeach()

    # add a task to the "my_xbeach_project" project
    task1 = simulator.run(input_dir=input_dir,
                          sim_config_filename="params.txt")

# task2 will be added to the default project
task2 = simulator.run(input_dir=input_dir,
                      sim_config_filename="params.txt")

print(task1.get_info().project) # "my_xbeach_project"
print(task2.get_info().project) # "userab1cdef2" (default project)

At any moment, the user can query what project is currently open for task submission by calling the get_current_project function from the projects module:

>>> inductiva.projects.get_current_project()
None
>>> with inductiva.projects.Project("my_xbeach_project", append=True):
...     inductiva.projects.get_current_project().name
my_xbeach_project

Listing Tasks in a Project#

Each project has a list of tasks that belong to it. To list all tasks in a given object, use the get_tasks method of the Project class:

>>> import inductiva
>>> project = inductiva.projects.Project("my_xbeach_project")
>>>> tasks = project.get_tasks()
[<inductiva.tasks.task.Task at 0x123f22110>,
 ...
 <inductiva.tasks.task.Task at 0x123f00190>]

Alternatively, one can use the inductiva tasks list CLI command to list all tasks of a project using the -p/--project filter argument:

$ inductiva tasks list -p my_xbeach_project
Showing tasks for project: my_xbeach_project.

 ID                          SIMULATOR   STATUS     SUBMITTED          STARTED            COMPUTATION TIME     RESOURCE TYPE
 2qnbmu6jxnf4zv8ma0c19ujhe   xbeach      success    24 May, 15:31:37   24 May, 15:31:45   0:00:01              GCP c2-standard-4
 [...]
 q3k3ad1etqdwwfw31die00orw   xbeach      success    24 May, 13:11:21   24 May, 13:11:21   0:00:06              GCP c2-standard-4

Thread awareness of Projects#

Projects are thread-aware. Multiple threads can define and use different projects simultaneously. A project opened in one thread will not affect the project opened in another thread. If a thread does not open a project, the default one will be used, even though the main thread might have opened a project other than the default one.

At any point, the user can get a reference to the currently active project in the calling thread using the inductiva.projects.get_current_project() function. This function will return the project that is currently open for task submission, or None if the default project is being used. The following snippet demonstrates how to use this function:

>>> import inductiva
>>> # so far, no project is open, hence "None" is returned
>>> inductiva.projects.get_current_project() is None
True
>>> # open the "demo" project and get a reference to it
>>> with inductiva.projects.Project('demo', append=True) as p:
...     print(inductiva.projects.get_current_project().name)
...     print(inductiva.projects.get_current_project().num_tasks)
...     print(inductiva.projects.get_current_project() is p)
demo
4
True
>>> # the "demo" project is closed, hence "None" is returned again
>>> inductiva.projects.get_current_project() is None
True

To illustrate the thread-awareness of projects, consider the following example. The main thread opens a project, and a new thread is created in the context of the opened project. The new thread retrieves the current project and prints its object just to find out that the default project is being used therein, even though the main thread has opened a project other than the default one:

import inductiva
import threading

def run():
    print(f"Thread 1: {inductiva.projects.get_current_project()=}")

with inductiva.projects.Project("demo", append=True):
    print(f"Main thread: {inductiva.projects.get_current_project()=}")
    thread = threading.Thread(target=run)
    thread.start()
    thread.join()

# would print:
# Main thread: inductiva.projects.get_current_project()=<inductiva.projects.project.Project object at 0x1242be910>
# Thread 1: inductiva.projects.get_current_project()=None