|
| 1 | +# Basic Tutorial |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +## Installation |
| 6 | +Install the python client and check the installation as follows: |
| 7 | + |
| 8 | +```command |
| 9 | +$ pip install osparc |
| 10 | +$ python -c "import osparc; print(osparc.__version__)" |
| 11 | +``` |
| 12 | + |
| 13 | + |
| 14 | +## Setup |
| 15 | + |
| 16 | +To setup the client, we need to provide a username and password to the configuration. These can be obtained in the UI under [Preferences > API Settings > API Keys](http://docs.osparc.io/#/docs/platform_introduction/main_window_and_navigation/user_setup/profile?id=preferences). Use the *API key* as username and the *API secret* as password. For security reasons, you should not write these values in your script but instead set them up via environment variables or read them from a separate file. In this example, we use environment variables which will be referred to as "OSPARC_API_KEY" and "OSPARC_API_SECRET" for the rest of the tutorial. |
| 17 | + |
| 18 | +```python |
| 19 | + |
| 20 | +import osparc |
| 21 | + |
| 22 | +cfg = osparc.Configuration( |
| 23 | + username=os.environ["OSPARC_API_KEY"], |
| 24 | + password=os.environ["OSPARC_API_SECRET"], |
| 25 | +) |
| 26 | +print(cfg.host) |
| 27 | + |
| 28 | +``` |
| 29 | + |
| 30 | +The configuration can now be used to create an instance of the API client. The API client is responsible of the communication with the osparc platform |
| 31 | + |
| 32 | + |
| 33 | +The functions in the [osparc API] are grouped into sections such as *meta*, *users*, *files* or *solvers*. Each section address a different resource of the platform. |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | +For example, the *users* section includes functions about the user (i.e. you) and can be accessed initializing a ``UsersApi``: |
| 38 | + |
| 39 | +```python |
| 40 | +import osparc |
| 41 | +from osparc.api import UsersApi |
| 42 | + |
| 43 | +with osparc.ApiClient(cfg) as api_client: |
| 44 | + |
| 45 | + users_api = UsersApi(api_client) |
| 46 | + |
| 47 | + profile = users_api.get_my_profile() |
| 48 | + print(profile) |
| 49 | + |
| 50 | + # |
| 51 | + # {'first_name': 'foo', |
| 52 | + # 'gravatar_id': 'aa33fssec77ea434c2ea4fb92d0fd379e', |
| 53 | + # 'groups': {'all': {'description': 'all users', |
| 54 | + # 'gid': '1', |
| 55 | + # 'label': 'Everyone'}, |
| 56 | + # 'me': {'description': 'primary group', |
| 57 | + # 'gid': '2', |
| 58 | + # 'label': 'foo'}, |
| 59 | + # 'organizations': []}, |
| 60 | + # 'last_name': '', |
| 61 | + |
| 62 | + # 'role': 'USER'} |
| 63 | + # |
| 64 | +``` |
| 65 | + |
| 66 | + |
| 67 | +## Solvers Workflow |
| 68 | + |
| 69 | +The osparc API can be used to execute any computational service published in the platform. This means that any computational service listed in the UI under the [Discover Tab](http://docs.osparc.io/#/docs/platform_introduction/core_elements/Discover?id=discover-tab) is accessible from the API. Note that computational services are denoted as *solvers* in the API for convenience, but they refer to the same concept. |
| 70 | + |
| 71 | + |
| 72 | +Let's use the sleepers computational service to illustrate a typical workflow. The sleepers computational service is a very basic service that simply waits (i.e. *sleeps*) a given time before producing some outputs. It takes as input one natural number, an optional text file input that contains another natural number and a boolean in the form of a checkbox. It also provides two outputs: one natural number and a file containing a single natural number. |
| 73 | + |
| 74 | + |
| 75 | +```python |
| 76 | +import osparc |
| 77 | + |
| 78 | +from osparc.api import FilesApi, SolversApi |
| 79 | +from osparc.models import File, Job, JobInputs, JobOutputs, JobStatus, Solver |
| 80 | + |
| 81 | + |
| 82 | +with osparc.ApiClient(cfg) as api_client: |
| 83 | + |
| 84 | + files_api = FilesApi(api_client) |
| 85 | + input_file: File = files_api.upload_file(file="file_with_number.txt") |
| 86 | + |
| 87 | + solvers_api = SolversApi(api_client) |
| 88 | + solver: Solver = solvers_api.get_solver_release( |
| 89 | + "simcore/services/comp/itis/sleeper", "2.0.2" |
| 90 | + ) |
| 91 | + |
| 92 | + job: Job = solvers_api.create_job( |
| 93 | + solver.id, |
| 94 | + solver.version, |
| 95 | + JobInputs( |
| 96 | + { |
| 97 | + "input_3": 0, |
| 98 | + "input_2": 3.0, |
| 99 | + "input_1": input_file, |
| 100 | + } |
| 101 | + ), |
| 102 | + ) |
| 103 | + |
| 104 | + status: JobStatus = solvers_api.start_job(solver.id, solver.version, job.id) |
| 105 | + while not status.stopped_at: |
| 106 | + time.sleep(3) |
| 107 | + status = solvers_api.inspect_job(solver.id, solver.version, job.id) |
| 108 | + print("Solver progress", f"{status.progress}/100", flush=True) |
| 109 | + # |
| 110 | + # Solver progress 0/100 |
| 111 | + # Solver progress 100/100 |
| 112 | + |
| 113 | + outputs: JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 114 | + |
| 115 | + print(f"Job {outputs.job_id} got these results:") |
| 116 | + for output_name, result in outputs.results.items(): |
| 117 | + print(output_name, "=", result) |
| 118 | + |
| 119 | + # |
| 120 | + # Job 19fc28f7-46fb-4e96-9129-5e924801f088 got these results: |
| 121 | + # |
| 122 | + # output_1 = {'checksum': '859fda0cb82fc4acb4686510a172d9a9-1', |
| 123 | + # 'content_type': 'text/plain', |
| 124 | + # 'filename': 'single_number.txt', |
| 125 | + # 'id': '9fb4f70e-3589-3e9e-991e-3059086c3aae'} |
| 126 | + # output_2 = 4.0 |
| 127 | + |
| 128 | + |
| 129 | + results_file: File = outputs.results["output_1"] |
| 130 | + download_path: str = files_api.download_file(file_id=results_file.id) |
| 131 | + print(Path(download_path).read_text()) |
| 132 | + # |
| 133 | + # 7 |
| 134 | + |
| 135 | +``` |
| 136 | + |
| 137 | +The script above |
| 138 | + |
| 139 | +1. Uploads a file ``file_with_number.txt`` |
| 140 | +2. Selects version ``2.0.2`` of the ``sleeper`` |
| 141 | +3. Runs the ``sleeper`` and provides a reference to the uploaded file and other values as input parameters |
| 142 | +4. Monitors the status of the solver while it is running in the platform |
| 143 | +5. When the execution completes, it checks the outputs |
| 144 | +6. One of the outputs is a file and it is downloaded |
| 145 | + |
| 146 | + |
| 147 | +#### Files |
| 148 | + |
| 149 | +Files used as input to solvers or produced by solvers in the platform are accessible in the **files** section and specifically with the ``FilesApi`` class. |
| 150 | +In order to use a file as input, it has to be uploaded first and the reference used in the corresponding solver's input. |
| 151 | + |
| 152 | + |
| 153 | +```python |
| 154 | +files_api = FilesApi(api_client) |
| 155 | +input_file: File = files_api.upload_file(file="file_with_number.txt") |
| 156 | + |
| 157 | + |
| 158 | +# ... |
| 159 | + |
| 160 | + |
| 161 | +outputs: JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 162 | +results_file: File = outputs.results["output_1"] |
| 163 | +download_path: str = files_api.download_file(file_id=results_file.id) |
| 164 | + |
| 165 | +``` |
| 166 | + |
| 167 | +In the snippet above, ``input_file`` is a ``File`` reference to the uploaded file and that is passed as input to the solver. Analogously, ``results_file`` is a ``File`` produced by the solver and that can also be downloaded. |
| 168 | + |
| 169 | + |
| 170 | +#### Solvers, Inputs and Outputs |
| 171 | + |
| 172 | +The inputs and outputs are specific for every solver. Every input/output has a name and an associated type that can be as simple as booleans, numbers, strings ... or more complex as files. You can find this information in the UI under Discover Tab, selecting the service card > More Info > raw metadata. For instance, the ``sleeper`` version ``2.0.2`` has the following ``raw-metadata``: |
| 173 | + |
| 174 | +```json |
| 175 | +{ |
| 176 | + inputs: { |
| 177 | + 'input_1': {'description': 'Pick a file containing only one ' |
| 178 | + 'integer', |
| 179 | + 'displayOrder': 1, |
| 180 | + 'fileToKeyMap': {'single_number.txt': 'input_1'}, |
| 181 | + 'label': 'File with int number', |
| 182 | + 'type': 'data:text/plain'}, |
| 183 | + 'input_2': {'defaultValue': 2, |
| 184 | + 'description': 'Choose an amount of time to sleep', |
| 185 | + 'displayOrder': 2, |
| 186 | + 'label': 'Sleep interval', |
| 187 | + 'type': 'integer', |
| 188 | + 'unit': 'second'}, |
| 189 | + 'input_3': {'defaultValue': False, |
| 190 | + 'description': 'If set to true will cause service to ' |
| 191 | + 'fail after it sleeps', |
| 192 | + 'displayOrder': 3, |
| 193 | + 'label': 'Fail after sleep', |
| 194 | + 'type': 'boolean'}, |
| 195 | + } |
| 196 | +} |
| 197 | +``` |
| 198 | +So, the inputs can be set as follows |
| 199 | + |
| 200 | +```python |
| 201 | +# ... |
| 202 | +job = solvers_api.create_job( |
| 203 | + solver.id, |
| 204 | + solver.version, |
| 205 | + job_inputs=JobInputs( |
| 206 | + { |
| 207 | + "input_1": uploaded_input_file, |
| 208 | + "input_2": 3 * n, # sleep time in secs |
| 209 | + "input_3": bool(n % 2), # fail after sleep? |
| 210 | + } |
| 211 | + ), |
| 212 | + ) |
| 213 | + |
| 214 | +``` |
| 215 | + |
| 216 | +And the metadata for the outputs are |
| 217 | +```json |
| 218 | +{ |
| 219 | + 'outputs': {'output_1': {'description': 'Integer is generated in range [1-9]', |
| 220 | + 'displayOrder': 1, |
| 221 | + 'fileToKeyMap': {'single_number.txt': 'output_1'}, |
| 222 | + 'label': 'File containing one random integer', |
| 223 | + 'type': 'data:text/plain'}, |
| 224 | + 'output_2': {'description': 'Interval is generated in range ' |
| 225 | + '[1-9]', |
| 226 | + 'displayOrder': 2, |
| 227 | + 'label': 'Random sleep interval', |
| 228 | + 'type': 'integer', |
| 229 | + 'unit': 'second'}}, |
| 230 | +} |
| 231 | +``` |
| 232 | +so this information determines which output corresponds to a number or a file in the following snippet |
| 233 | + |
| 234 | +```python |
| 235 | +# ... |
| 236 | + |
| 237 | +outputs: JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 238 | + |
| 239 | +output_file = outputs.results["output_1"] |
| 240 | +number = outputs.results["output_2"] |
| 241 | + |
| 242 | +assert status.state == "SUCCESS" |
| 243 | + |
| 244 | + |
| 245 | +assert isinstance(output_file, File) |
| 246 | +assert isinstance(number, float) |
| 247 | + |
| 248 | +# output file exists |
| 249 | +assert files_api.get_file(output_file.id) == output_file |
| 250 | + |
| 251 | +# can download and open |
| 252 | +download_path: str = files_api.download_file(file_id=output_file.id) |
| 253 | +assert float(Path(download_path).read_text()), "contains a random number" |
| 254 | +``` |
| 255 | + |
| 256 | +#### Job Status |
| 257 | + |
| 258 | +Once the client script triggers the solver, the solver runs in the platform and the script is freed. Sometimes, it is convenient to monitor the status of the run to see e.g. the progress of the execution or if the run was completed. |
| 259 | + |
| 260 | +A solver runs in a plaforma starts a ``Job``. Using the ``solvers_api``, allows us to inspect the ``Job`` and get a ``JobStatus`` with information about its status. For instance |
| 261 | + |
| 262 | +```python |
| 263 | + status: JobStatus = solvers_api.start_job(solver.id, solver.version, job.id) |
| 264 | + while not status.stopped_at: |
| 265 | + time.sleep(3) |
| 266 | + status = solvers_api.inspect_job(solver.id, solver.version, job.id) |
| 267 | + print("Solver progress", f"{status.progress}/100", flush=True) |
| 268 | +``` |
| 269 | + |
| 270 | +## References |
| 271 | + |
| 272 | +- [osparc API python client] documentation |
| 273 | +- [osparc API] documentation |
| 274 | +- A full script with this tutorial: [``sleeper.py``](https://github.com/ITISFoundation/osparc-simcore/blob/master/tests/public-api/examples/sleeper.py) |
| 275 | + |
| 276 | +[osparc API python client]:https://itisfoundation.github.io/osparc-simcore-python-client |
| 277 | +[osparc API]:https://api.staging.osparc.io/doc |
0 commit comments