Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ™ octavia-cli: generate open api client #9277

Merged
merged 20 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
default_language_version:
python: python3.7
python: python3

repos:
- repo: https://github.com/johann-petrak/licenseheaders.git
rev: v0.8.8
hooks:
- id: licenseheaders
args: ["--tmpl=LICENSE_SHORT", "--ext=py", "-f"]

- repo: https://github.com/ambv/black
rev: 21.11b1
hooks:
- id: black

- repo: https://github.com/timothycrosley/isort
rev: 5.10.1
hooks:
- id: isort
args: ["--dont-follow-links", "--jobs=-1"]
additional_dependencies: ["colorama"]

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.5.0
hooks:
Expand All @@ -29,7 +26,8 @@ repos:
(?x)^.*(
.github/|
source_specs.yaml|
destination_specs.yaml
destination_specs.yaml|
.gitlab-ci.yml
).?$

- repo: https://github.com/csachs/pyproject-flake8
Expand All @@ -38,12 +36,14 @@ repos:
- id: pyproject-flake8
additional_dependencies: ["mccabe"]
alias: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.910-1
hooks:
- id: mypy

exclude: |
(?x)^.*(
octavia-cli/unit_tests/|
).?$
- repo: local
hooks:
- id: spec-linter
Expand Down
2 changes: 2 additions & 0 deletions airbyte-api/src/main/openapi/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1944,8 +1944,10 @@ components:
$ref: "#/components/schemas/Notification"
firstCompletedSync:
type: boolean
nullable: true
Copy link
Contributor

Choose a reason for hiding this comment

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

what are you trying to achieve here? because these fields are not in the required list above they should already be nullable and should not require the extra nullable field.

Copy link
Contributor Author

@alafanechere alafanechere Jan 11, 2022

Choose a reason for hiding this comment

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

When I called the workspace endpoint with the generated API client it raises an error during serialization of the response because it expected this field to be not nullable. When I make this change in config.yaml the problem is bypassed. I'm going to check if I can find another solution by tweaking some generator options, otherwise, I'll have to make these kinds of changes in config.yaml for other endpoints that will be used in the future, which is not ideal.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah. that seems like kinda big deal. swagger generation is always a little jenky around the edges, but nullable fields is a pretty core feature, so it seems like a really bad sign if the generator can't handle it because it means there will be other problems.

one thing to test is instead of using the nullable field if instead setting type: [boolean, null] works. I don't like this as much as the pattern we already use but if we needed to switch to this pattern it may not be the end of the world.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found this issue OpenAPITools/openapi-generator#4816 on OpenApiTools repo, this is for their C# generator but it looks like the exact same problem I have with the Python one. I'll continue to investigate and try type: [boolean, null]. Let me know if you think that I should rather implement the client myself.

Copy link
Contributor

Choose a reason for hiding this comment

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

we definitely don't want to implement it ourselves. my guess is the C# bug is language specific (since C# is actually a typed language)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried [boolean, null] but this is not a valid spec according to open-api-generator.
I think I found a workaround. I can disable the type validation of the response. It also disables deserialization to a model and leads us to manipulate the raw response as a python dict. It's not the most elegant solution but I prefer this rather than editing the API spec. Related commit: a586719
I also reverted my changes on airbyte-api's config.yaml.

feedbackDone:
type: boolean
nullable: true
WorkspaceUpdate:
type: object
required:
Expand Down
1 change: 1 addition & 0 deletions octavia-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.coverage
.venv
airbyte_api_client/
7 changes: 4 additions & 3 deletions octavia-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ The project is under development: readers can refer to our [tech spec deck](http
We encourage users to use the CLI with docker to avoid the hassle of setting up a Python installation.
The project is under development: we have not yet published any docker image to our Docker registry.

1. Build the image locally:
1. Build the image locally (from the root of the repo):
```bash
docker build -t octavia-cli:dev --rm .
SUB_BUILD=OCTAVIA_CLI ./gradlew build #from the root of the repo
```
2. Run the CLI from docker:
```bash
Expand All @@ -38,10 +38,11 @@ Summary of achievements:

| Date | Milestone |
|------------|-------------------------------------|
| 2022-01-06 | Generate an API Python client from our Open API spec |
| 2021-12-22 | Bootstrapping the project's code base |

# Developing locally
1. Install Python 3.10.0. We suggest doing it through `pyenv`
1. Install Python 3.8.12. We suggest doing it through `pyenv`
2. Create a virtualenv: `python -m venv .venv`
3. Activate the virtualenv: `source .venv/bin/activate`
4. Install dev dependencies: `pip install -e .\[dev\]`
Expand Down
20 changes: 20 additions & 0 deletions octavia-cli/bin/generate-api-client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

set -e

[ -z "$ROOT_DIR" ] && exit 1

OPENAPI_CONFIG_PATH=airbyte-api/src/main/openapi/config.yaml
GENERATED_CLIENT_PATH=octavia-cli/airbyte_api_client

function main() {
rm -rf "$ROOT_DIR/$GENERATED_CLIENT_PATH"/*.py

docker run --user "$(id -u):$(id -g)" -v "$ROOT_DIR":/airbyte openapitools/openapi-generator-cli generate \
-i "/airbyte/$OPENAPI_CONFIG_PATH" \
-o "/airbyte/$GENERATED_CLIENT_PATH" \
-g python \
--additional-properties=packageName=airbyte_api_client
}

main "$@"
9 changes: 9 additions & 0 deletions octavia-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ airbytePython {
moduleDirectory 'octavia_cli'
}

task generateApiClient(type: Exec) {
Copy link
Contributor

Choose a reason for hiding this comment

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

does the caching work properly on this task? the expectation should be that if airbyte-api/src/main/openapi/config.yaml does not change that this task does not need to run again. if that's not set up properly it will require re-executing this task and every tasks that depends on them (which is most of them).

see UP-TO-DATE in https://docs.gradle.org/current/userguide/more_about_tasks.html

Copy link
Contributor Author

@alafanechere alafanechere Jan 11, 2022

Choose a reason for hiding this comment

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

Thanks for this suggestion! I realized I could use a grade plugin already used for java API client generation: org.openapitools.generator.gradle.plugin.tasks.GenerateTask. It supports incremental builds, but I can't figure why it's not working in my context.
I did the following in b285449:

  • update octavia-cli/build.gradle to use the GenerateTask.
  • exclude the generated client from the license headers checks.
  • remove the script I wrote to generate the client with the docker image.

Still, I don't get why I can't see the UP-TO-DATE flag after consequent builds.

Copy link
Contributor Author

@alafanechere alafanechere Jan 12, 2022

Choose a reason for hiding this comment

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

I finally made the FROM-CACHE and UP-TO-DATE mechanisms work. The solution was to use the buildDir as output dir. Using other paths made Gradle re-run the task on each run. The generated client is now stored in octavia-cli/build/airbyte_api_client. Related commit: e95abc9

I confirm that changes in airbyte-api/src/main/openapi/config.yaml will trigger a regeneration of the client on SUB_BUILD=OCTAVIA_CLI ./gradlew build.

Copy link
Contributor

Choose a reason for hiding this comment

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

awesome!

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like you to present this very briefly in the dev meeting. This is something we often get wrong when using gradle, so it would be good to have a 5 minute presentation to share the knowledge with the team.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok! To be honest, I'm not understanding why using input paths for tasks that are not buildDir does not lead to UP-TO-DATE but it could indeed be interesting to share this finding and to open this discussion in the dev meeting.

environment 'ROOT_DIR', rootDir.absolutePath
commandLine 'bin/generate-api-client.sh'
}

blackFormat.dependsOn generateApiClient
isortFormat.dependsOn generateApiClient
flakeCheck.dependsOn generateApiClient
installReqs.dependsOn generateApiClient
40 changes: 26 additions & 14 deletions octavia-cli/octavia_cli/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,53 @@
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
#

import airbyte_api_client
import click
from airbyte_api_client.api import workspace_api


@click.group()
@click.option("--airbyte-url", envvar="AIRBYTE_URL", default="http://localhost:8000", help="The URL of your Airbyte instance.")
def octavia(airbyte_url):
# TODO: check if the airbyte_url is reachable
click.secho(f"πŸ™ - Octavia is targetting your Airbyte instance running at {airbyte_url}")
@click.pass_context
def octavia(ctx: click.Context, airbyte_url: str) -> None:
ctx.ensure_object(dict)
client_configuration = airbyte_api_client.Configuration(host=f"{airbyte_url}/api")
api_client = airbyte_api_client.ApiClient(client_configuration)
# TODO alafanechere workspace check might deserve its own function
api_instance = workspace_api.WorkspaceApi(api_client)
api_response = api_instance.list_workspaces()
# TODO alafanechere prompt user to chose a workspace if multiple workspaces exist
workspace_id = api_response.workspaces[0].workspace_id
click.echo(f"πŸ™ - Octavia is targetting your Airbyte instance running at {airbyte_url} on workspace {workspace_id}")
ctx.obj["API_CLIENT"] = api_client
ctx.obj["WORKSPACE_ID"] = workspace_id


@octavia.command(help="Scaffolds a local project directories.")
def init():
def init() -> None:
raise click.ClickException("The init command is not yet implemented.")


@octavia.command(name="list", help="List existing resources on the Airbyte instance.")
def _list():
raise click.ClickException("The init command is not yet implemented.")
def _list() -> None:
raise click.ClickException("The list command is not yet implemented.")


@octavia.command(name="import", help="Import an existing resources from the Airbyte instance.")
def _import():
raise click.ClickException("The init command is not yet implemented.")
def _import() -> None:
raise click.ClickException("The import command is not yet implemented.")


@octavia.command(help="Generate a YAML configuration file to manage a resource.")
def create():
raise click.ClickException("The init command is not yet implemented.")
def create() -> None:
raise click.ClickException("The create command is not yet implemented.")


@octavia.command(help="Create or update resources according to YAML configurations.")
def apply():
raise click.ClickException("The init command is not yet implemented.")
def apply() -> None:
raise click.ClickException("The apply command is not yet implemented.")


@octavia.command(help="Delete resources")
def delete():
raise click.ClickException("The init command is not yet implemented.")
def delete() -> None:
raise click.ClickException("The delete command is not yet implemented.")
5 changes: 3 additions & 2 deletions octavia-cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
#

import os
import pathlib

from setuptools import find_packages, setup
Expand Down Expand Up @@ -39,8 +40,8 @@
"Source": "https://github.com/airbytehq/airbyte",
"Tracker": "https://github.com/airbytehq/airbyte/issues",
},
packages=find_packages(exclude=("tests", "docs")),
install_requires=["click~=8.0.3"],
packages=find_packages(exclude=("unit_tests", "docs")),
install_requires=["click~=8.0.3", f"airbyte_api_client @ file://{os.getcwd()}/airbyte_api_client"],
python_requires=">=3.8.12",
extras_require={
"dev": ["MyPy~=0.812", "pytest~=6.2.5", "pytest-cov", "pytest-mock", "requests-mock", "pre-commit"],
Expand Down
27 changes: 24 additions & 3 deletions octavia-cli/unit_tests/test_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
#

from unittest import mock

import click
import pytest
from click.testing import CliRunner
from octavia_cli import entrypoint


def test_octavia():
@click.command()
@click.pass_context
def dumb(ctx):
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ˜‚

pass


@mock.patch("octavia_cli.entrypoint.workspace_api")
@mock.patch("octavia_cli.entrypoint.airbyte_api_client")
def test_octavia(mock_airbyte_api_client: mock.Mock, mock_workspace_api: mock.Mock):
context_object = {}
mock_api_instance = mock_workspace_api.WorkspaceApi.return_value
mock_api_instance.list_workspaces.return_value = mock.MagicMock(workspaces=[mock.MagicMock(workspace_id="expected_workspace_id")])

entrypoint.octavia.add_command(dumb)
runner = CliRunner()
result = runner.invoke(entrypoint.octavia)
result = runner.invoke(entrypoint.octavia, ["--airbyte-url", "test-airbyte-url", "dumb"], obj=context_object)
mock_airbyte_api_client.Configuration.assert_called_with(host="test-airbyte-url/api")
mock_airbyte_api_client.ApiClient.assert_called_with(mock_airbyte_api_client.Configuration.return_value)
mock_workspace_api.WorkspaceApi.assert_called_with(mock_airbyte_api_client.ApiClient.return_value)
mock_api_instance.list_workspaces.assert_called_once()
assert context_object["API_CLIENT"] == mock_airbyte_api_client.ApiClient.return_value
assert context_object["WORKSPACE_ID"] == "expected_workspace_id"
assert result.exit_code == 0
assert result.output.startswith("Usage: octavia [OPTIONS] COMMAND [ARGS]...")


@pytest.mark.parametrize(
Expand Down
1 change: 1 addition & 0 deletions tools/python/.flake8
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exclude =
.eggs # python libraries"
.tox
build
airbyte_api_client # generated api client in octavia-cli
extend-ignore =
E203, # whitespace before ':' (conflicts with Black)
E231, # Bad trailing comma (conflicts with Black)
Expand Down