Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

recipe_run SQLModel -> FastAPI -> CRUD Client & CLI #6

Merged
merged 49 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
73dceb3
Initial commit
cisaacstern Oct 4, 2021
ec5ffce
first database pr commit
cisaacstern Nov 16, 2021
1f3b27e
Merge remote-tracking branch 'charles/main' into database
cisaacstern Nov 16, 2021
9035035
remove merge annotation from .gitignore
cisaacstern Nov 16, 2021
218955e
SQLModel/FastAPI Hero example
cisaacstern Nov 16, 2021
dd9ce1d
add server and cli modules
cisaacstern Nov 17, 2021
0916a1c
cli test first draft
cisaacstern Nov 19, 2021
254eaf1
client draft
cisaacstern Nov 19, 2021
891372e
add model factory funcs
cisaacstern Nov 20, 2021
5dedcf9
fix workflow pr branch trigger
cisaacstern Nov 20, 2021
cc91aa4
generalize api funcs
cisaacstern Nov 21, 2021
dfdfdd1
generalize api creation with MultipleModels + GenerateEndpoints objects
cisaacstern Nov 22, 2021
16c3af6
match client + cli creation funcs to api, and generalize/parametrize …
cisaacstern Nov 23, 2021
238c8c2
make model factories methods of MultipleModels
cisaacstern Nov 23, 2021
8c7ff8c
move abstractions to standalone module
cisaacstern Nov 24, 2021
80d5b5d
generalize update + delete tests
cisaacstern Nov 25, 2021
0b18bc4
tests rewrite, make db in process cwd, crud methods
cisaacstern Dec 6, 2021
f2f9f41
revert pre-commit to 19.10b0
cisaacstern Dec 6, 2021
5789ff0
rename test module
cisaacstern Dec 6, 2021
6a13aa9
swap hero example for recipe_run first draft
cisaacstern Dec 7, 2021
4d85280
make database sub-cli & add docstrings forit
cisaacstern Dec 7, 2021
a1f30cc
Merge branch 'pangeo-forge:main' into database
cisaacstern Dec 7, 2021
2d7b200
docstrings for client dataclass
cisaacstern Dec 7, 2021
546d234
test abstract funcs
cisaacstern Dec 7, 2021
9228e09
pre-commit run --all-files
cisaacstern Dec 7, 2021
a8b6db0
all entrypoints for read test
cisaacstern Dec 7, 2021
81bf321
test registration
cisaacstern Dec 7, 2021
0abaee8
test read, update, and delete nonexistent
cisaacstern Dec 7, 2021
9b723df
add dev guide docs
cisaacstern Dec 8, 2021
1b97aff
Update pangeo_forge_orchestrator/abstractions.py
cisaacstern Dec 9, 2021
dd8666d
only test register_endpoints (not _RegisterEndpoints)
cisaacstern Dec 15, 2021
881e44b
add ModelKwargs + ModelFixture classes for tests
cisaacstern Dec 15, 2021
88f5029
add create_func lazy fixture
cisaacstern Dec 15, 2021
566daa8
add TestCreate & test all create errors with raises
cisaacstern Dec 15, 2021
a1b0fb9
move create failure tests into TestCreate
cisaacstern Dec 15, 2021
2b8e28c
add TestReadRange container
cisaacstern Dec 16, 2021
d60ab09
TestReadRange -> TestRead
cisaacstern Dec 16, 2021
ad1d0cc
fixture containers, CRUD test classes
cisaacstern Dec 16, 2021
5141edc
org per-entrypoint CRUD funcs together; all fixtures in conftest
cisaacstern Dec 16, 2021
b81f1b3
rename entrypoint -> interface in tests
cisaacstern Dec 16, 2021
c089105
abstractions docstrings + models comment
cisaacstern Dec 16, 2021
9471721
Update tests/conftest.py
cisaacstern Dec 16, 2021
7ddd177
typo fix
cisaacstern Dec 16, 2021
cb8ed6f
make RecipeRunRead model comment a docstring
cisaacstern Jan 6, 2022
72a24ca
refactor tests with mixins
cisaacstern Jan 7, 2022
c6d073e
add explanation for custom error classes
cisaacstern Jan 7, 2022
3e0f540
clear database in fixtures, not tests
cisaacstern Jan 7, 2022
051523b
reduce len(ModelFixture.kwargs) to 2 and explain why
cisaacstern Jan 7, 2022
c582ca6
fix two typos
cisaacstern Jan 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions docs/development_guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# Development Guide

## Install

For a development installation of `pangeo-forge-orchestrator`, start by cloning the repository.

Next, start a new `conda` environment with Python >= 3.8 and < 3.10.

Finally, from the repo root `git checkout` whichever branch you plan to develop on, then run

```bash
pip install -e '.[dev]'
```
to install the Python package, CLI entrypoint, and all dev dependencies.

## Start a local API dev server

In a new, empty directory outside of the project repo, activate the `conda` environment in which `pangeo-forge-orchestrator` is installed.
Then run

```bash
uvicorn pangeo_forge_orchestrator.api:api
```
to start an API development server.

> This command will also create a new `database.db` sqlite database file in your current working directory and populate that database with
blank tables for all models defined in the `MODELS` dictionary defined in `pangeo_forge_orchestrator.models`.

Calling `uvicorn --help` provides various options you can add to this command. Two worth noting:

- **`--reload`**: Enables hot reloads. Because our `uvicorn` process is not running in the package repository, enabling hot
reloads requires specifying the package repository path via the `--reload-dir` option. Enabling hot reloading might therefore look like
```bash
uvicorn pangeo_forge_orchestrator.api:api --reload --reload-dir=$PFO_REPO_PATH
```
where `PFO_REPO_PATH` is an environment variable pointing to the absolute path for `~/pangeo-forge-orchestrator/pangeo_forge_orchestrator/`.
- **`--port`**: The API server starts at `http://127.0.0.1:8000` by default. If your local port `8000` is occupied you can pass an alternate port number here.

## Checkout the API in a browser

> In what follows, we will assume your local API is running at `http://127.0.0.1:8000`. If you selected an alternate port, adjust the below URLs accordingly.

With the local API server running, you may now review the auto-generated API documentation by navigating to [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/) in your browser. (The same documentation in a different style is also available at [http://127.0.0.1:8000/redoc/](http://127.0.0.1:8000/redoc/).)

For any given API endpoint (i.e., route) described in the documentation, you will note that navigating to that endpoint in the browser shows simply an empty list. For example, the browser just renders `[]` when we navigate to [http://127.0.0.1:8000/recipe_runs/](http://127.0.0.1:8000/recipe_runs/).

> If you do not see an empty list at this endpoint, that's likely because the directory your API is running in already had a sqlite file called `database.db` in it when the server started. This is an easy oversight to make, if you are starting and stopping servers in the same directory during a development session, given that the server does not delete the sqlite file when it is stopped.

## Interfaces

To perform any of the create, read, update, and delete (CRUD) operations on our database, we can choose whether to interface via the command line interface (CLI) or the Python client. Both of these interfaces provide the same functionality and cooresponding functions have the same name in both interfaces.

### Client basics

To initialize a client for your local API server, execute

```python
>>> from pangeo_forge_orchestrator.client import Client

>>> client = Client(base_url="http://127.0.0.1:8000")
```
in your Python interpreter of choice.

### CLI basics

To get started with the CLI, make sure the `conda` environment in which `pangeo-forge-orchestrator` is installed is activated, and run

```bash
pangeo-forge database --help
```
to return a top-level listing of the CRUD function names. For each of the listed functions, similarly calling

```bash
pangeo-forge database $FUNC_NAME --help
```
will return a description of the arguments and/or options supported by the function.

To use any of these functions, the CLI will need to know the URL where the API server can be found. We communicate this by
setting the `PANGEO_FORGE_DATABASE_URL` environment variable:
```bash
export PANGEO_FORGE_DATABASE_URL='http://127.0.0.1:8000'
```

## CRUD operations

### Create

#### With client

Creating entries in a database table with many required fields, due to its relative verboseness, is likely easiest via the Python client.
Here's how to create an entry in the `recipe_run` table, using the `client` defined in the previous section.

First we inspect the creation schema demonstrated in the API documentation.
For the `/recipe_runs/` endpoint, the API documentation's example JSON looks like this:

```json
{
"recipe_id": "string",
"run_date": "2021-12-07T23:52:08.336Z",
"bakery_id": 0,
"feedstock_id": 0,
"commit": "string",
"version": "string",
"status": "string",
"path": "string",
"message": "string"
}
```

Creating and posting such a request with the Python client is therefore as simple as:

```python

>>> json = {
"recipe_id": "my-recipe-id",
"run_date": "2021-12-07T23:52:08.336Z",
"bakery_id": 1, # Note that `bakery_id` and `feedstock_id` are placeholder fields
"feedstock_id": 1, # which will eventually link to other tables in the database
"commit": "abcdefg1234567",
"version": "1.0",
"status": "complete",
"path": "/path-to-dataset.zarr",
"message": "An optional message.",
}
>>> response = client.post("/recipe_runs/", json=json)
>>> response.status_code
200
>>> response.json()
{'recipe_id': 'my-recipe-id',
'run_date': '2021-12-07T23:52:08.336000',
'bakery_id': 1,
'feedstock_id': 1,
'commit': 'abcdefg1234567',
'version': '1.0',
'status': 'complete',
'path': '/path-to-dataset.zarr',
'message': 'An optional message.',
'id': 1}
```

Note that the addition of the `id` field in the API's response tells us the index of the entry in the database.

#### From CLI

While the length of this particular command may encourage sticking to the Python client for create requests, it's worth
noting that POST is also supported from the CLI. To create a second entry in the `recipe_run` table from the CLI, we run:

```console
$ pangeo-forge database post "/recipe_runs/" '{"recipe_id": "the-second-recipe-id", "run_date": "2021-12-07T23:52:08.336Z", "bakery_id": 1, "feedstock_id": 1, "commit": "abcdefg1234567", "version": "1.0", "status": "In progress", "path": "/path-to-second-dataset.zarr"}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the same vein

Suggested change
$ pangeo-forge database post "/recipe_runs/" '{"recipe_id": "the-second-recipe-id", "run_date": "2021-12-07T23:52:08.336Z", "bakery_id": 1, "feedstock_id": 1, "commit": "abcdefg1234567", "version": "1.0", "status": "In progress", "path": "/path-to-second-dataset.zarr"}'
$ pangeo-forge database recipe_run create --json '{"recipe_id": "the-second-recipe-id", "run_date": "2021-12-07T23:52:08.336Z", "bakery_id": 1, "feedstock_id": 1, "commit": "abcdefg1234567", "version": "1.0", "status": "In progress", "path": "/path-to-second-dataset.zarr"}'


{'recipe_id': 'the-second-recipe-id', 'run_date': '2021-12-07T23:52:08.336000', 'bakery_id': 1, 'feedstock_id': 1, 'commit': 'abcdefg1234567', 'version': '1.0', 'status': 'In progress', 'path': '/path-to-second-dataset.zarr', 'message': None, 'id': 2}
```
The `response.json()` dictionary echoed to stdout is the entry as it now appears in the database.
(Our request omitted the optional `'message'` field, therefore it is returned from the database as `None`.
This is the second entry we've created in the table, therefore its `'id'` is `2`.)

### Read

#### With client

Now that we have posted some data to our table, we can read back that whole table with:

```python
>>> client.get("/recipe_runs/").json()
[{'recipe_id': 'my-recipe-id',
'run_date': '2021-12-07T23:52:08.336000',
'bakery_id': 1,
'feedstock_id': 1,
'commit': 'abcdefg1234567',
'version': '1.0',
'status': 'complete',
'path': '/path-to-dataset.zarr',
'message': 'An optional message.',
'id': 1},
{'recipe_id': 'the-second-recipe-id',
'run_date': '2021-12-07T23:52:08.336000',
'bakery_id': 1,
'feedstock_id': 1,
'commit': 'abcdefg1234567',
'version': '1.0',
'status': 'In progress',
'path': '/path-to-second-dataset.zarr',
'message': None,
'id': 2}]
```

Or, if we just want to select a single entry from the table, we can append the `id` of that entry to
the path passed to the same `client.get` request:

```python
>>> client.get("/recipe_runs/1").json()
{'recipe_id': 'my-recipe-id',
'run_date': '2021-12-07T23:52:08.336000',
'bakery_id': 1,
'feedstock_id': 1,
'commit': 'abcdefg1234567',
'version': '1.0',
'status': 'complete',
'path': '/path-to-dataset.zarr',
'message': 'An optional message.',
'id': 1}
```

#### From CLI

The same function can be performed from the CLI. For a list of all entries in the table:

```console
$ pangeo-forge database get "/recipe_runs/"

[{'recipe_id': 'my-recipe-id', 'run_date': '2021-12-07T23:52:08.336000', 'bakery_id': 1, 'feedstock_id': 1, 'commit': 'abcdefg1234567', 'version': '1.0', 'status': 'complete', 'path': '/path-to-dataset.zarr', 'message': 'An optional message.', 'id': 1}, {'recipe_id': 'the-second-recipe-id', 'run_date': '2021-12-07T23:52:08.336000', 'bakery_id': 1, 'feedstock_id': 1, 'commit': 'abcdefg1234567', 'version': '1.0', 'status': 'In progress', 'path': '/path-to-second-dataset.zarr', 'message': None, 'id': 2}]
```

Or, for just a single entry, append its `id` to the request path:
```console
$ pangeo-forge database get "/recipe_runs/1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'm not a huge fan of specifying the endpoint as a string. Would kind of prefer a syntax like

Suggested change
$ pangeo-forge database get "/recipe_runs/1"
$ pangeo-forge database recipe_run get 1


{'recipe_id': 'my-recipe-id', 'run_date': '2021-12-07T23:52:08.336000', 'bakery_id': 1, 'feedstock_id': 1, 'commit': 'abcdefg1234567', 'version': '1.0', 'status': 'complete', 'path': '/path-to-dataset.zarr', 'message': 'An optional message.', 'id': 1}
```

### Update

#### With client

Let's say we want to change the `recipe_id` field of the first `recipe_run` table entry. Currently, it's assigned as:

```python
>>> client.get("/recipe_runs/1").json()["recipe_id"]
'my-recipe-id'
```
We can use the client's `patch` method to change it, as follows:
```python
>>> client.patch("/recipe_runs/1", json=dict(recipe_id="corrected-recipe-id"))
<Response [200]>
>>> client.get("/recipe_runs/1").json()["recipe_id"]
'corrected-recipe-id'
```

The dictionary passed to `client.patch` is not limited to one field, as shown here; it can contain as many fields as you would like.

#### From CLI

The CLI interface for `patch` is almost identical:

```console
$ pangeo-forge database patch "/recipe_runs/1" '{"recipe_id": "fixed-twice-id"}'

{'recipe_id': 'fixed-twice-id', 'run_date': '2021-12-07T23:52:08.336000', 'bakery_id': 1, 'feedstock_id': 1, 'commit': 'abcdefg1234567', 'version': '1.0', 'status': 'complete', 'path': '/path-to-dataset.zarr', 'message': 'An optional message.', 'id': 1}
```


### Delete

#### With client

To delete an entry with the client, simply pass its `id`-terminated endpoint to `client.delete`:

```python
>>> client.delete("/recipe_runs/1")
<Response [200]>
>>> client.get("/recipe_runs/1").json()
{'detail': 'RecipeRun not found'}
```

#### From CLI

The CLI delete interface takes the same argument as the client. Simply pass it an `id`-terminated endpoint:

```console
$ pangeo-forge database delete "/recipe_runs/2"
{'ok': True}
$ pangeo-forge database get "/recipe_runs/2"
{'detail': 'RecipeRun not found'}
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ You have found the documentation for for `pangeo-forge-orchestrator`, an unrelea
```{toctree}
:maxdepth: 1

development_guide
```
Empty file.
Loading