Skip to content

Conversation

nicoschmdt
Copy link
Contributor

@nicoschmdt nicoschmdt commented Jul 23, 2025

fix #1866

Move versionchooser endpoints to FastApi;
Remove legacy frontend folder from core/services/versionchooser;
Update frontend upload logic for FastAPI backend compatibility.

To test use the following docker image: nicoschmdt/blueos-core:aiohttp-to-fastapi

Summary by Sourcery

Migrate VersionChooser from connexion/aiohttp to FastAPI, replacing legacy code and updating handlers, dependencies, frontend logic, and tests for FastAPI compatibility

New Features:

  • Migrate VersionChooser service to FastAPI with versioned API routers and uvicorn server

Bug Fixes:

  • Fix frontend upload component to use FormData and updated FastAPI endpoint path

Enhancements:

  • Replace aiohttp.web handlers with FastAPI JSONResponse and StreamingResponse
  • Refactor Docker login endpoints to use Pydantic models and PrettyJSONResponse
  • Add command-line argument support for debug mode, host, and port

Build:

  • Remove connexion and aiohttp dependencies; add fastapi, fastapi-versioning, and uvicorn

Tests:

  • Update tests to assert on response.body and status_code for FastAPI compatibility

Chores:

  • Remove legacy frontend directory and static file serving in core/services/versionchooser

Copy link

sourcery-ai bot commented Jul 23, 2025

Reviewer's Guide

This PR fully migrates the Version Chooser service from connexion/aiohttp to FastAPI, replaces all aiohttp response types with FastAPI responses, removes legacy frontend assets and static handlers, updates the Vue upload logic, adapts tests for the new response interfaces, refactors Docker login models to Pydantic, and updates project dependencies accordingly.

Class diagram for DockerLoginInfo refactor to Pydantic model

classDiagram
    class DockerLoginInfo {
        +bool root = True
        +str username = ""
        +str password = ""
        +str registry = DEFAULT_DOCKER_REGISTRY
    }
    DockerLoginInfo <|-- pydantic.BaseModel
Loading

Class diagram for new CommandLineArgs dataclass

classDiagram
    class CommandLineArgs {
        +bool debug
        +str host
        +int port
        +from_args() CommandLineArgs
    }
Loading

Class diagram for new FastAPI routers and VersionChooser usage

classDiagram
    class VersionChooser {
        +get_version()
        +set_version(image, tag)
        +pull_version(repository, tag)
        +delete_version(image, tag)
        +get_available_local_versions()
        +get_available_versions(repository)
        +load(data)
        +restart()
        +get_bootstrap_version()
        +set_bootstrap_version(tag)
    }
    class version_router_v1
    class bootstrap_router_v1
    class docker_router_v1
    class index_router_v1
    version_router_v1 --> VersionChooser : Depends
    bootstrap_router_v1 --> VersionChooser : Depends
    docker_router_v1 --> DockerLoginInfo
    docker_router_v1 --> get_docker_accounts
    docker_router_v1 --> make_docker_login
    docker_router_v1 --> make_docker_logout
Loading

File-Level Changes

Change Details Files
Main service bootstrapped with FastAPI and versioned routers
  • Replaced connexion.AioHttpApp with FastAPI application in main
  • Introduced uvicorn Config/Server and CommandLineArgs for CLI server settings
  • Extracted route definitions into api/app.py and versioned routers under api/v1
  • Removed aiohttp-based route setup and static file server
  • Enabled FastAPI versioning and mounted static assets
core/services/versionchooser/main.py
core/services/versionchooser/api/app.py
core/services/versionchooser/api/v1/routers/index.py
core/services/versionchooser/api/v1/routers/docker.py
core/services/versionchooser/api/v1/routers/version.py
core/services/versionchooser/api/v1/routers/bootstrap.py
core/services/versionchooser/api/v1/routers/__init__.py
core/services/versionchooser/args.py
Replaced aiohttp responses with FastAPI JSONResponse/StreamingResponse
  • Changed all VersionChooser methods to return JSONResponse or StreamingResponse
  • Replaced web.Response, web.json_response, web.StreamResponse imports and calls
  • Implemented async generator for streaming pull output
  • Updated import statements for fastapi.Response, JSONResponse, StreamingResponse
core/services/versionchooser/utils/chooser.py
Removed legacy frontend handlers and folder
  • Deleted FRONTEND_FOLDER and STATIC_FOLDER definitions
  • Removed index() method serving static index.html
  • Dropped core/services/versionchooser/frontend directory and related imports
core/services/versionchooser/utils/chooser.py
core/services/versionchooser/main.py
Updated frontend upload logic to FormData
  • Wrapped file input in FormData before POST
  • Adjusted endpoint URL to remove trailing slash
  • Removed manual Content-Type header for multipart upload
core/frontend/src/components/version-chooser/VersionChooser.vue
Adapted tests to FastAPI response attributes
  • Replaced response.text with response.body.decode()
  • Replaced result.status with result.status_code in assertions
core/services/versionchooser/test_versionchooser.py
Refactored Docker login to Pydantic and PrettyJSONResponse
  • Converted DockerLoginInfo from dataclass to Pydantic BaseModel
  • Removed custom from_json constructor and asdict usage
  • Replaced aiohttp web.json_response with PrettyJSONResponse
core/services/versionchooser/docker_login.py
Updated project dependencies for FastAPI stack
  • Removed connexion and aiohttp swagger-ui dependency
  • Added fastapi, fastapi-versioning, uvicorn to dependencies
  • Locked pyproject.toml to include new packages
core/services/versionchooser/pyproject.toml

Assessment against linked issues

Issue Objective Addressed Explanation
#1866 Remove yaml and using the same api libraries that we are using

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@CLAassistant
Copy link

CLAassistant commented Jul 23, 2025

CLA assistant check
All committers have signed the CLA.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @nicoschmdt - I've reviewed your changes - here's some feedback:

  • Consider refactoring the repeated JSONResponse error and success constructions in VersionChooser into a small helper to reduce duplication and centralize response formatting.
  • The StreamingResponse media_type is set to "application/x-www-form-urlencoded"—you may want to switch to a JSON or newline-delimited JSON content type to better reflect the payload.
  • The routers create a new aiodocker.Docker client and VersionChooser per request—verify this request-scoped lifecycle meets your performance and resource-cleanup requirements.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider refactoring the repeated JSONResponse error and success constructions in VersionChooser into a small helper to reduce duplication and centralize response formatting.
- The StreamingResponse media_type is set to "application/x-www-form-urlencoded"—you may want to switch to a JSON or newline-delimited JSON content type to better reflect the payload.
- The routers create a new aiodocker.Docker client and VersionChooser per request—verify this request-scoped lifecycle meets your performance and resource-cleanup requirements.

## Individual Comments

### Comment 1
<location> `core/services/versionchooser/api/v1/routers/version.py:71` </location>
<code_context>
+
+
+@version_router_v1.post("/load", summary="Load a docker tar file")
+async def load(file: UploadFile = File(...), version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
+    data = await file.read()
+    return await version_chooser.load(data)
</code_context>

<issue_to_address>
The load endpoint reads the entire file into memory, which may cause issues with large uploads.

Consider processing the uploaded file in chunks or streaming it to the Docker client to avoid high memory usage and improve scalability.
</issue_to_address>

### Comment 2
<location> `core/services/versionchooser/api/v1/routers/docker.py:22` </location>
<code_context>
+)
+
+
+@docker_router_v1.post("/login", summary="Login Docker daemon to a registry")
+async def docker_login(request: DockerLoginInfo) -> None:
+    return make_docker_login(request)
</code_context>

<issue_to_address>
The docker_login endpoint is defined as async but returns a synchronous function.

Consider making 'make_docker_login' async or running it in a thread pool to maintain consistency and avoid potential blocking issues.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
@docker_router_v1.post("/login", summary="Login Docker daemon to a registry")
async def docker_login(request: DockerLoginInfo) -> None:
    return make_docker_login(request)
=======
import asyncio

@docker_router_v1.post("/login", summary="Login Docker daemon to a registry")
async def docker_login(request: DockerLoginInfo) -> None:
    return await asyncio.to_thread(make_docker_login, request)
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `core/services/versionchooser/api/v1/routers/docker.py:27` </location>
<code_context>
+    return make_docker_login(request)
+
+
+@docker_router_v1.post("/logout", summary="Logout Docker daemon from a registry")
+async def docker_logout(request: DockerLoginInfo) -> Any:
+    return make_docker_logout(request)
</code_context>

<issue_to_address>
The docker_logout endpoint is async but returns a synchronous function.

Consider making 'make_docker_logout' async or running it in a thread pool to prevent blocking the event loop if it performs I/O.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
@docker_router_v1.post("/logout", summary="Logout Docker daemon from a registry")
async def docker_logout(request: DockerLoginInfo) -> Any:
    return make_docker_logout(request)
=======
import asyncio

@docker_router_v1.post("/logout", summary="Logout Docker daemon from a registry")
async def docker_logout(request: DockerLoginInfo) -> Any:
    return await asyncio.to_thread(make_docker_logout, request)
>>>>>>> REPLACE

</suggested_fix>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Member

@Williangalvani Williangalvani left a comment

Choose a reason for hiding this comment

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

Looks good to me =]

I have mixed feelings about the unused static folder...
Not a blocker though.

@joaomariolago joaomariolago merged commit 7045d2a into bluerobotics:master Jul 29, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

version-chooser: Catch up with others services
4 participants