|
| 1 | +# Basic Tutorial |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +## Installation |
| 6 | +Install the python client and check the installation as follows: |
| 7 | + |
| 8 | + |
| 9 | +```python |
| 10 | +import importlib |
| 11 | +if importlib.util.find_spec('osparc') is not None: |
| 12 | + ! pip install osparc |
| 13 | +! python -c "import osparc; print(osparc.__version__)" |
| 14 | +``` |
| 15 | + |
| 16 | +## Setup |
| 17 | + |
| 18 | +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](https://docs.osparc.io/#/docs/platform_introduction/user_setup/security_details?id=generating-o%c2%b2s%c2%b2parc-tokens). 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. |
| 19 | + |
| 20 | + |
| 21 | +```python |
| 22 | + |
| 23 | +import os |
| 24 | +from osparc import Configuration |
| 25 | + |
| 26 | +cfg = Configuration( |
| 27 | + host=os.environ["OSPARC_API_HOST"], |
| 28 | + username=os.environ["OSPARC_API_KEY"], |
| 29 | + password=os.environ["OSPARC_API_SECRET"], |
| 30 | +) |
| 31 | +print(cfg.host) |
| 32 | + |
| 33 | +``` |
| 34 | + |
| 35 | +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 |
| 36 | + |
| 37 | + |
| 38 | +The functions in the [osparc API](https://api.osparc.io/dev/doc#/) are grouped into sections such as *meta*, *users*, *files* or *solvers*. Each section address a different resource of the platform. |
| 39 | + |
| 40 | + |
| 41 | + |
| 42 | +For example, the *users* section includes functions about the user (i.e. you) and can be accessed initializing a ``UsersApi``: |
| 43 | + |
| 44 | + |
| 45 | +```python |
| 46 | +from osparc import ApiClient, UsersApi |
| 47 | + |
| 48 | +with ApiClient(cfg) as api_client: |
| 49 | + |
| 50 | + users_api = UsersApi(api_client) |
| 51 | + |
| 52 | + profile = users_api.get_my_profile() |
| 53 | + print(profile) |
| 54 | + |
| 55 | + # |
| 56 | + # {'first_name': 'foo', |
| 57 | + # 'gravatar_id': 'aa33fssec77ea434c2ea4fb92d0fd379e', |
| 58 | + # 'groups': {'all': {'description': 'all users', |
| 59 | + # 'gid': '1', |
| 60 | + # 'label': 'Everyone'}, |
| 61 | + # 'me': {'description': 'primary group', |
| 62 | + # 'gid': '2', |
| 63 | + # 'label': 'foo'}, |
| 64 | + # 'organizations': []}, |
| 65 | + # 'last_name': '', |
| 66 | + # 'login': 'foo@itis.swiss', |
| 67 | + # 'role': 'USER'} |
| 68 | + # |
| 69 | +``` |
| 70 | + |
| 71 | +## Solvers Workflow |
| 72 | + |
| 73 | +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 [Services Tab](https://docs.osparc.io/#/docs/platform_introduction/services) 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. |
| 74 | + |
| 75 | + |
| 76 | +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. |
| 77 | + |
| 78 | + |
| 79 | +```python |
| 80 | +import time |
| 81 | +from pathlib import Path |
| 82 | +from zipfile import ZipFile |
| 83 | +from tempfile import TemporaryDirectory |
| 84 | + |
| 85 | +import osparc |
| 86 | + |
| 87 | +Path("file_with_number.txt").write_text("3") |
| 88 | + |
| 89 | +with osparc.ApiClient(cfg) as api_client: |
| 90 | + |
| 91 | + files_api = osparc.FilesApi(api_client) |
| 92 | + input_file: osparc.File = files_api.upload_file(file="file_with_number.txt") |
| 93 | + |
| 94 | + solvers_api = osparc.SolversApi(api_client) |
| 95 | + solver: osparc.Solver = solvers_api.get_solver_release( |
| 96 | + "simcore/services/comp/itis/sleeper", "2.1.6" |
| 97 | + ) |
| 98 | + |
| 99 | + job: osparc.Job = solvers_api.create_job( |
| 100 | + solver.id, |
| 101 | + solver.version, |
| 102 | + osparc.JobInputs( |
| 103 | + { |
| 104 | + "input_4": 2, |
| 105 | + "input_3": "false", |
| 106 | + "input_2": 3, |
| 107 | + "input_1": input_file, |
| 108 | + } |
| 109 | + ), |
| 110 | + ) |
| 111 | + |
| 112 | + status: osparc.JobStatus = solvers_api.start_job(solver.id, solver.version, job.id) |
| 113 | + while not status.stopped_at: |
| 114 | + time.sleep(3) |
| 115 | + status = solvers_api.inspect_job(solver.id, solver.version, job.id) |
| 116 | + print("Solver progress", f"{status.progress}/100", flush=True) |
| 117 | + assert status.state == "SUCCESS" |
| 118 | + # |
| 119 | + # Solver progress 0/100 |
| 120 | + # Solver progress 100/100 |
| 121 | + |
| 122 | + outputs: osparc.JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 123 | + |
| 124 | + print(f"Job {outputs.job_id} got these results:") |
| 125 | + for output_name, result in outputs.results.items(): |
| 126 | + print(output_name, "=", result) |
| 127 | + |
| 128 | + # |
| 129 | + # Job 19fc28f7-46fb-4e96-9129-5e924801f088 got these results: |
| 130 | + # |
| 131 | + # output_1 = {'checksum': '859fda0cb82fc4acb4686510a172d9a9-1', |
| 132 | + # 'content_type': 'text/plain', |
| 133 | + # 'filename': 'single_number.txt', |
| 134 | + # 'id': '9fb4f70e-3589-3e9e-991e-3059086c3aae'} |
| 135 | + # output_2 = 4.0 |
| 136 | + |
| 137 | + logfile_path: str = solvers_api.get_job_output_logfile( |
| 138 | + solver.id, solver.version, job.id |
| 139 | + ) |
| 140 | + zip_path = Path(logfile_path) |
| 141 | + |
| 142 | + with TemporaryDirectory() as tmp_dir: |
| 143 | + with ZipFile(f"{zip_path}") as fzip: |
| 144 | + fzip.extractall(tmp_dir) |
| 145 | + logfiles = list(Path(tmp_dir).glob("*.log*")) |
| 146 | + print("Unzipped", logfiles[0], "contains:\n", logfiles[0].read_text()) |
| 147 | + # |
| 148 | + # Unzipped extracted/sleeper_2.0.2.logs contains: |
| 149 | + # 2022-06-01T18:15:00.405035847+02:00 Entrypoint for stage production ... |
| 150 | + # 2022-06-01T18:15:00.421279969+02:00 User : uid=0(root) gid=0(root) groups=0(root) |
| 151 | + # 2022-06-01T18:15:00.421560331+02:00 Workdir : /home/scu |
| 152 | + # ... |
| 153 | + # 2022-06-01T18:15:00.864550043+02:00 |
| 154 | + # 2022-06-01T18:15:03.923876794+02:00 Will sleep for 3 seconds |
| 155 | + # 2022-06-01T18:15:03.924473521+02:00 [PROGRESS] 1/3... |
| 156 | + # 2022-06-01T18:15:03.925021846+02:00 Remaining sleep time 0.9999995231628418 |
| 157 | + # 2022-06-01T18:15:03.925558026+02:00 [PROGRESS] 2/3... |
| 158 | + # 2022-06-01T18:15:03.926103062+02:00 Remaining sleep time 0.9999985694885254 |
| 159 | + # 2022-06-01T18:15:03.926643184+02:00 [PROGRESS] 3/3... |
| 160 | + # 2022-06-01T18:15:03.933544384+02:00 Remaining sleep time 0.9999983310699463 |
| 161 | + |
| 162 | + download_path: str = files_api.download_file(file_id=outputs.results["output_1"].id) |
| 163 | + print(Path(download_path).read_text()) |
| 164 | + # |
| 165 | + # 7 |
| 166 | + |
| 167 | +``` |
| 168 | + |
| 169 | +The script above |
| 170 | + |
| 171 | +1. Uploads a file ``file_with_number.txt`` |
| 172 | +2. Selects version ``2.0.2`` of the ``sleeper`` |
| 173 | +3. Runs the ``sleeper`` and provides a reference to the uploaded file and other values as input parameters |
| 174 | +4. Monitors the status of the solver while it is running in the platform |
| 175 | +5. When the execution completes, it checks the outputs |
| 176 | +6. The logs are downloaded, unzipped and saved to a new ```extracted``` directory |
| 177 | +7. One of the outputs is a file and it is downloaded |
| 178 | + |
| 179 | + |
| 180 | +#### Files |
| 181 | + |
| 182 | +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. |
| 183 | +In order to use a file as input, it has to be uploaded first and the reference used in the corresponding solver's input. |
| 184 | + |
| 185 | +```python |
| 186 | +files_api = FilesApi(api_client) |
| 187 | +input_file: File = files_api.upload_file(file="file_with_number.txt") |
| 188 | + |
| 189 | + |
| 190 | +# ... |
| 191 | + |
| 192 | + |
| 193 | +outputs: JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 194 | +results_file: File = outputs.results["output_1"] |
| 195 | +download_path: str = files_api.download_file(file_id=results_file.id) |
| 196 | +``` |
| 197 | + |
| 198 | +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. |
| 199 | + |
| 200 | + |
| 201 | +#### Solvers, Inputs and Outputs |
| 202 | + |
| 203 | +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 Services Tab, selecting the service card > Information > Raw metadata. For instance, the ``sleeper`` version ``2.1.6`` has the following ``raw-metadata``: |
| 204 | + |
| 205 | +```json |
| 206 | +{ |
| 207 | + "inputs": { |
| 208 | + "input_1": { |
| 209 | + "displayOrder": 1, |
| 210 | + "label": "File with int number", |
| 211 | + "description": "Pick a file containing only one integer", |
| 212 | + "type": "data:text/plain", |
| 213 | + "fileToKeyMap": { |
| 214 | + "single_number.txt": "input_1" |
| 215 | + }, |
| 216 | + "keyId": "input_1" |
| 217 | + }, |
| 218 | + "input_2": { |
| 219 | + "unitLong": "second", |
| 220 | + "unitShort": "s", |
| 221 | + "label": "Sleep interval", |
| 222 | + "description": "Choose an amount of time to sleep in range [0-65]", |
| 223 | + "keyId": "input_2", |
| 224 | + "displayOrder": 2, |
| 225 | + "type": "ref_contentSchema", |
| 226 | + "contentSchema": { |
| 227 | + "title": "Sleep interval", |
| 228 | + "type": "integer", |
| 229 | + "x_unit": "second", |
| 230 | + "minimum": 0, |
| 231 | + "maximum": 65 |
| 232 | + }, |
| 233 | + "defaultValue": 2 |
| 234 | + }, |
| 235 | + "input_3": { |
| 236 | + "displayOrder": 3, |
| 237 | + "label": "Fail after sleep", |
| 238 | + "description": "If set to true will cause service to fail after it sleeps", |
| 239 | + "type": "boolean", |
| 240 | + "defaultValue": false, |
| 241 | + "keyId": "input_3" |
| 242 | + }, |
| 243 | + "input_4": { |
| 244 | + "unitLong": "meter", |
| 245 | + "unitShort": "m", |
| 246 | + "label": "Distance to bed", |
| 247 | + "description": "It will first walk the distance to bed", |
| 248 | + "keyId": "input_4", |
| 249 | + "displayOrder": 4, |
| 250 | + "type": "ref_contentSchema", |
| 251 | + "contentSchema": { |
| 252 | + "title": "Distance to bed", |
| 253 | + "type": "integer", |
| 254 | + "x_unit": "meter" |
| 255 | + }, |
| 256 | + "defaultValue": 0 |
| 257 | + } |
| 258 | + } |
| 259 | +``` |
| 260 | + |
| 261 | +So, the inputs can be set as follows |
| 262 | + |
| 263 | +```python |
| 264 | +# ... |
| 265 | +job: osparc.Job = solvers_api.create_job( |
| 266 | + solver.id, |
| 267 | + solver.version, |
| 268 | + osparc.JobInputs( |
| 269 | + { |
| 270 | + "input_4": 2, |
| 271 | + "input_3": "false", |
| 272 | + "input_2": 3, |
| 273 | + "input_1": input_file, |
| 274 | + } |
| 275 | + ), |
| 276 | +) |
| 277 | +``` |
| 278 | + |
| 279 | +And the metadata for the outputs are |
| 280 | + |
| 281 | +```json |
| 282 | + "output_1": { |
| 283 | + "displayOrder": 1, |
| 284 | + "label": "File containing one random integer", |
| 285 | + "description": "Integer is generated in range [1-9]", |
| 286 | + "type": "data:text/plain", |
| 287 | + "fileToKeyMap": { |
| 288 | + "single_number.txt": "output_1" |
| 289 | + }, |
| 290 | + "keyId": "output_1" |
| 291 | + }, |
| 292 | + "output_2": { |
| 293 | + "unitLong": "second", |
| 294 | + "unitShort": "s", |
| 295 | + "label": "Random sleep interval", |
| 296 | + "description": "Interval is generated in range [1-9]", |
| 297 | + "keyId": "output_2", |
| 298 | + "displayOrder": 2, |
| 299 | + "type": "ref_contentSchema", |
| 300 | + "contentSchema": { |
| 301 | + "title": "Random sleep interval", |
| 302 | + "type": "integer", |
| 303 | + "x_unit": "second" |
| 304 | + } |
| 305 | +``` |
| 306 | + |
| 307 | +so this information determines which output corresponds to a number or a file in the following snippet |
| 308 | + |
| 309 | +```python |
| 310 | +# ... |
| 311 | + |
| 312 | +outputs: JobOutputs = solvers_api.get_job_outputs(solver.id, solver.version, job.id) |
| 313 | + |
| 314 | +output_file = outputs.results["output_1"] |
| 315 | +number = outputs.results["output_2"] |
| 316 | + |
| 317 | +assert status.state == "SUCCESS" |
| 318 | + |
| 319 | + |
| 320 | +assert isinstance(output_file, File) |
| 321 | +assert isinstance(number, float) |
| 322 | + |
| 323 | +# output file exists |
| 324 | +assert files_api.get_file(output_file.id) == output_file |
| 325 | + |
| 326 | +# can download and open |
| 327 | +download_path: str = files_api.download_file(file_id=output_file.id) |
| 328 | +assert float(Path(download_path).read_text()), "contains a random number" |
| 329 | + |
| 330 | +``` |
| 331 | + |
| 332 | +#### Job Status |
| 333 | + |
| 334 | +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. |
| 335 | + |
| 336 | +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 |
| 337 | + |
| 338 | +```python |
| 339 | + status: JobStatus = solvers_api.start_job(solver.id, solver.version, job.id) |
| 340 | + while not status.stopped_at: |
| 341 | + time.sleep(3) |
| 342 | + status = solvers_api.inspect_job(solver.id, solver.version, job.id) |
| 343 | + print("Solver progress", f"{status.progress}/100", flush=True) |
| 344 | +``` |
| 345 | + |
| 346 | +#### Logs |
| 347 | + |
| 348 | +When a solver runs, it will generate logs during execution which are then saved as .log files. Starting from the osparc Python Client version 0.5.0, The ``solvers_api`` also allows us to obtain the ``logfile_path`` associated with a particular ``Job``. This is a zip file that can then be extracted and saved. For instance |
| 349 | + |
| 350 | +```python |
| 351 | +logfile_path: str = solvers_api.get_job_output_logfile( |
| 352 | + solver.id, solver.version, job.id |
| 353 | +) |
| 354 | +zip_path = Path(logfile_path) |
| 355 | + |
| 356 | +extract_dir = Path("./extracted") |
| 357 | +extract_dir.mkdir() |
| 358 | + |
| 359 | +with ZipFile(f"{zip_path}") as fzip: |
| 360 | + fzip.extractall(f"{extract_dir}") |
| 361 | +``` |
| 362 | + |
| 363 | +## References |
| 364 | + |
| 365 | +- [osparc API python client] documentation |
| 366 | +- [osparc API] documentation |
| 367 | +- A full script with this tutorial: [``sleeper.py``](https://github.com/ITISFoundation/osparc-simcore/blob/master/tests/public-api/examples/sleeper.py) |
| 368 | + |
| 369 | +[osparc API python client]:https://itisfoundation.github.io/osparc-simcore-clients/#/ |
| 370 | +[osparc API]:https://api.osparc.io/dev/doc#/ |
| 371 | +[Download as BasicTutorial_v0.5.0.ipynb](clients/python/docs/BasicTutorial_v0.5.0.ipynb ":ignore title") |
0 commit comments