diff --git a/.github/workflows/build-spring-api.yml b/.github/workflows/build-spring-api.yml index c5bc0b5..2b7ed46 100644 --- a/.github/workflows/build-spring-api.yml +++ b/.github/workflows/build-spring-api.yml @@ -49,7 +49,7 @@ jobs: if: always() with: name: Spring API Tests - path: services/spring-api/build/test-results/test/*.xml + path: services/spring-api/build/test-results/**/*.xml reporter: java-junit - name: Upload coverage HTML diff --git a/api/openapi-internal.yaml b/api/openapi-internal.yaml new file mode 100644 index 0000000..de59500 --- /dev/null +++ b/api/openapi-internal.yaml @@ -0,0 +1,416 @@ +openapi: 3.1.1 +info: + title: Cooking Assistant GenAI Services API (Internal) + version: 1.0.0 + description: Private backend contract between the Spring API gateway and the Python GenAI microservices. + +servers: + - url: http://localhost:8080 + +tags: + - name: Help Service + description: Endpoints managed by the py-help-service container + - name: Recipe Service + description: Endpoints managed by the py-recipe-service container + +paths: + # --------------------------------------------- + # INFRASTRUCTURE ENDPOINTS (SHARED) + # --------------------------------------------- + /health: + get: + tags: + - Help Service + - Recipe Service + summary: Service Health Check + description: Verifies that the targeted internal microservice instance is responding normally. + responses: + "200": + description: Service is healthy + content: + application/json: + schema: + type: object + required: [status] + additionalProperties: false + properties: + status: + type: string + example: "healthy" + + # --------------------------------------------- + # HELP SERVICE ENDPOINTS + # --------------------------------------------- + /ai/help: + post: + tags: [Help Service] + summary: Process contextual help request via LLM + description: Receives a cooking query bundled with rich user profile constraints and active recipe schemas to build safe prompts. + parameters: + - $ref: "#/components/parameters/X-Internal-Timestamp" + - $ref: "#/components/parameters/X-Internal-Signature" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/HelpRequestForwarded" + responses: + "200": + description: AI help generation completed successfully + content: + application/json: + schema: + $ref: "#/components/schemas/HelpResponse" + "400": + description: Invalid request payload syntax or corrupted internal header data (e.g. non-numeric timestamp) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "401": + description: Unauthorized due to missing headers or an expired timestamp window + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "403": + description: Forbidden due to an invalid cryptographic HMAC validation mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "504": + description: Upstream LangChain LLM orchestration layer timed out + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + # --------------------------------------------- + # RECIPE SERVICE ENDPOINTS + # --------------------------------------------- + /ai/recipes: + post: + tags: [Recipe Service] + summary: Generate cooking recipes via LLM + description: Forwards user prompt queries along with their profiles to create new recipe suggestions. + parameters: + - $ref: "#/components/parameters/X-Internal-Timestamp" + - $ref: "#/components/parameters/X-Internal-Signature" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RecipeRequestForwarded" + responses: + "200": + description: AI recipe generation completed successfully + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecipeInput" + "400": + description: Invalid prompt processing structure + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "401": + description: Missing security headers + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "403": + description: HMAC signature mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "504": + description: Upstream LangChain LLM orchestration layer timed out + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + +components: + # --------------------------------------------- + # SECURITY HEADERS (HMAC Architecture) + # --------------------------------------------- + parameters: + X-Internal-Timestamp: + name: X-Internal-Timestamp + in: header + required: true + description: Linux Unix epoch timestamp string binding the generation window to block replay attacks. + schema: + type: string + X-Internal-Signature: + name: X-Internal-Signature + in: header + required: true + description: Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. + schema: + type: string + + # --------------------------------------------- + # DATA SCHEMAS + # --------------------------------------------- + + schemas: + ErrorResponse: + type: object + required: [message] + additionalProperties: false + properties: + message: + type: string + minLength: 1 + + AuthResponse: + type: object + required: [token] + additionalProperties: false + properties: + token: + type: string + minLength: 1 + description: JWT bearer token to include in subsequent requests + + AuthRequest: + additionalProperties: false + required: [username, password] + allOf: + - $ref: "#/components/schemas/UserCredentials" + + UserProfile: + type: object + required: [username, preferences] + additionalProperties: false + properties: + username: + type: string + minLength: 1 + preferences: + $ref: "#/components/schemas/UserPreferences" + + UserProfileUpdate: + additionalProperties: false + description: At least one field must be provided + allOf: + - $ref: "#/components/schemas/UserCredentials" + - type: object + properties: + preferences: + $ref: "#/components/schemas/UserPreferences" + + UserCredentials: + type: object + description: Reusable field definitions for username and password constraints + properties: + username: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[a-zA-Z0-9_.-]+$" + description: Alphanumeric, underscores, hyphens, and dots only + password: + type: string + minLength: 4 + maxLength: 128 + + UserPreferences: + type: object + additionalProperties: false + properties: + language: + type: string + enum: [EN, DE, HU] + description: Preferred UI and AI-content language as an ISO 639-1 code + diet: + type: array + items: + type: string + description: Dietary restriction or style (e.g. vegan, keto) + allergies: + type: array + items: + type: string + description: List of ingredients the user is allergic to + aboutMe: + type: array + items: + type: string + description: Free-form user context provided to the AI + + RecipeIngredient: + type: object + required: [quantity, unit, name] + additionalProperties: false + properties: + quantity: + type: number + minimum: 0 + unit: + type: string + minLength: 1 + description: Unit of measurement (e.g. g, ml, cup, tbsp) + name: + type: string + minLength: 1 + + RecipeNutrients: + type: object + required: [calories, protein, fat, carbs] + additionalProperties: false + properties: + calories: + type: integer + minimum: 0 + description: Number of Calories (kcal) + protein: + type: integer + minimum: 0 + description: Protein in grams + fat: + type: integer + minimum: 0 + description: Fat in grams + carbs: + type: integer + minimum: 0 + description: Carbohydrates in grams + + RecipeInput: + type: object + required: [title, ingredients, instructions, portions] + additionalProperties: false + properties: + title: + type: string + minLength: 1 + maxLength: 255 + ingredients: + type: array + items: + $ref: "#/components/schemas/RecipeIngredient" + minItems: 1 + instructions: + type: array + items: + type: string + minLength: 1 + minItems: 1 + portions: + type: number + minimum: 0.5 + nutrients: + $ref: "#/components/schemas/RecipeNutrients" + + RecipeUpdate: + type: object + additionalProperties: false + description: At least one field must be provided + properties: + title: + type: string + minLength: 1 + maxLength: 255 + ingredients: + type: array + items: + $ref: "#/components/schemas/RecipeIngredient" + minItems: 1 + instructions: + type: array + items: + type: string + minLength: 1 + minItems: 1 + portions: + type: number + minimum: 0.5 + nutrients: + $ref: "#/components/schemas/RecipeNutrients" + + RecipeCreated: + type: object + required: [id] + properties: + id: + type: integer + format: int64 + minimum: 1 + + Recipe: + type: object + additionalProperties: false + allOf: + - $ref: "#/components/schemas/RecipeInput" + - type: object + required: [id] + properties: + id: + type: integer + format: int64 + minimum: 1 + + RecipeRequest: + type: object + required: [prompt] + additionalProperties: false + properties: + prompt: + type: string + minLength: 1 + maxLength: 4096 + + HelpRequest: + type: object + required: [recipe, prompt] + additionalProperties: false + properties: + recipe: + $ref: "#/components/schemas/RecipeInput" + prompt: + type: string + minLength: 1 + maxLength: 4096 + + HelpResponse: + type: object + required: [response] + additionalProperties: false + properties: + response: + type: string + minLength: 1 + + HelpRequestForwarded: + type: object + required: [profile, prompt] + additionalProperties: false + properties: + profile: + $ref: "#/components/schemas/UserProfile" + recipe: + $ref: "#/components/schemas/RecipeInput" + prompt: + type: string + minLength: 1 + + RecipeRequestForwarded: + type: object + required: [profile, prompt] + additionalProperties: false + properties: + profile: + $ref: "#/components/schemas/UserProfile" + prompt: + type: string + minLength: 1 diff --git a/api/openapi.yaml b/api/openapi.yaml index af4c2ef..7ca2a90 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -663,17 +663,6 @@ components: minLength: 1 maxLength: 4096 - RecipeRequestForwarded: - type: object - required: [profile, prompt] - additionalProperties: false - properties: - profile: - $ref: "#/components/schemas/UserProfile" - prompt: - type: string - minLength: 1 - HelpRequest: type: object required: [recipe, prompt] @@ -686,19 +675,6 @@ components: minLength: 1 maxLength: 4096 - HelpRequestForwarded: - type: object - required: [profile, recipe, prompt] - additionalProperties: false - properties: - profile: - $ref: "#/components/schemas/UserProfile" - recipe: - $ref: "#/components/schemas/RecipeInput" - prompt: - type: string - minLength: 1 - HelpResponse: type: object required: [response] diff --git a/api/scripts/gen-all.sh b/api/scripts/gen-all.sh index 61928b3..3bfef19 100755 --- a/api/scripts/gen-all.sh +++ b/api/scripts/gen-all.sh @@ -1,11 +1,39 @@ #!/usr/bin/env bash set -euo pipefail -KOTLIN_POST_PROCESS_FILE="ktlint -F" openapi-generator-cli generate -i api/openapi.yaml -g kotlin-spring --additional-properties="useSpringBoot4=true,useTags=true,interfaceOnly=true" --enable-post-process-file -o services/spring-api - -openapi-python-client generate --path api/openapi.yaml --output-path services/py-help-service/client --overwrite -openapi-python-client generate --path api/openapi.yaml --output-path services/py-recipe-service/client --overwrite +KOTLIN_POST_PROCESS_FILE="ktlint -F" openapi-generator-cli generate \ + -i api/openapi.yaml \ + -g kotlin-spring \ + --additional-properties="useSpringBoot4=true,useTags=true,interfaceOnly=true,apiPackage=org.openapitools.api,modelPackage=org.openapitools.model" \ + --type-mappings=number=kotlin.Double \ + --enable-post-process-file \ + -o services/spring-api npx openapi-typescript api/openapi.yaml -o web-client/src/api.ts -# Keep this script in sync with .gitattributes. +openapi-generator-cli generate \ + -i api/openapi-internal.yaml \ + -g kotlin \ + --global-property="apis,models" \ + --additional-properties="library=jvm-retrofit2,apiPackage=org.openapitools.internal.client,modelPackage=org.openapitools.internal.model,infrastructurePackage=org.openapitools.internal.client" \ + --type-mappings=number=kotlin.Double \ + --import-mappings=BigDecimal=java.lang.Double \ + -o services/spring-api + +# delete lines causing compilation failures because of missing references +sed -i '' '/org.openapitools.client.infrastructure/d' services/spring-api/src/main/kotlin/org/openapitools/internal/client/*ServiceApi.kt + +# delete generated tests for generated code because of compatibility issues +rm -rf services/spring-api/src/test/kotlin/org/openapitools/internal/ + +# Generates the internal servers/client sdks for the Python Help Service. +openapi-python-client generate \ + --path api/openapi-internal.yaml \ + --output-path services/py-help-service/client \ + --overwrite + +# Generates the internal servers/client sdks for the Python Recipe Service. +openapi-python-client generate \ + --path api/openapi-internal.yaml \ + --output-path services/py-recipe-service/client \ + --overwrite diff --git a/docker-compose.yml b/docker-compose.yml index 19de073..a5006c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,3 +84,17 @@ services: target: /app/vite.config.ts - action: rebuild path: ./web-client/package.json + + swagger-ui: + image: swaggerapi/swagger-ui:v5.18.2 + container_name: team-devsecops-swagger-ui + ports: + - "8083:8080" + environment: + URLS: "[ + { url: '/openapi/openapi.yaml', name: 'Public Gateway API' }, + { url: '/openapi/openapi-internal.yaml', name: 'Internal GenAI Microservices' } + ]" + volumes: + - ./api/openapi.yaml:/usr/share/nginx/html/openapi/openapi.yaml:ro + - ./api/openapi-internal.yaml:/usr/share/nginx/html/openapi/openapi-internal.yaml:ro \ No newline at end of file diff --git a/services/py-help-service/client/README.md b/services/py-help-service/client/README.md index b84f1b8..ce9c766 100644 --- a/services/py-help-service/client/README.md +++ b/services/py-help-service/client/README.md @@ -1,11 +1,11 @@ -# cooking-assistant-api-client -A client library for accessing Cooking Assistant API +# cooking-assistant-gen-ai-services-api-internal-client +A client library for accessing Cooking Assistant GenAI Services API (Internal) ## Usage First, create a client: ```python -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client client = Client(base_url="https://api.example.com") ``` @@ -13,7 +13,7 @@ client = Client(base_url="https://api.example.com") If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: ```python -from cooking_assistant_api_client import AuthenticatedClient +from cooking_assistant_gen_ai_services_api_internal_client import AuthenticatedClient client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") ``` @@ -21,9 +21,9 @@ client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSec Now call your endpoint and use your models: ```python -from cooking_assistant_api_client.models import MyDataModel -from cooking_assistant_api_client.api.my_tag import get_my_data_model -from cooking_assistant_api_client.types import Response +from cooking_assistant_gen_ai_services_api_internal_client.models import MyDataModel +from cooking_assistant_gen_ai_services_api_internal_client.api.my_tag import get_my_data_model +from cooking_assistant_gen_ai_services_api_internal_client.types import Response with client as client: my_data: MyDataModel = get_my_data_model.sync(client=client) @@ -34,9 +34,9 @@ with client as client: Or do the same thing with an async version: ```python -from cooking_assistant_api_client.models import MyDataModel -from cooking_assistant_api_client.api.my_tag import get_my_data_model -from cooking_assistant_api_client.types import Response +from cooking_assistant_gen_ai_services_api_internal_client.models import MyDataModel +from cooking_assistant_gen_ai_services_api_internal_client.api.my_tag import get_my_data_model +from cooking_assistant_gen_ai_services_api_internal_client.types import Response async with client as client: my_data: MyDataModel = await get_my_data_model.asyncio(client=client) @@ -72,14 +72,14 @@ Things to know: 1. All path/query params, and bodies become method arguments. 1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) -1. Any endpoint which did not have a tag will be in `cooking_assistant_api_client.api.default` +1. Any endpoint which did not have a tag will be in `cooking_assistant_gen_ai_services_api_internal_client.api.default` ## Advanced customizations There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): ```python -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") @@ -100,7 +100,7 @@ You can even set the httpx client directly, but beware that this will override a ```python import httpx -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client client = Client( base_url="https://api.example.com", diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py b/services/py-help-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py deleted file mode 100644 index 840086f..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py +++ /dev/null @@ -1,170 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs( - recipe_id: int, -) -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "delete", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 204: - response_204 = cast(Any, None) - return response_204 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py b/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py deleted file mode 100644 index 16c86fd..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py +++ /dev/null @@ -1,139 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "get", - "url": "/recipes", - } - - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | list[Recipe] | None: - if response.status_code == 200: - response_200 = [] - _response_200 = response.json() - for response_200_item_data in _response_200: - response_200_item = Recipe.from_dict(response_200_item_data) - - response_200.append(response_200_item) - - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | list[Recipe]]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | list[Recipe]]: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | list[Recipe]] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> ErrorResponse | list[Recipe] | None: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | list[Recipe] - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | list[Recipe]]: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | list[Recipe]] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> ErrorResponse | list[Recipe] | None: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | list[Recipe] - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py b/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py deleted file mode 100644 index ce02eba..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py +++ /dev/null @@ -1,174 +0,0 @@ -from http import HTTPStatus -from typing import Any -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...types import Response - - -def _get_kwargs( - recipe_id: int, -) -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "get", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | Recipe | None: - if response.status_code == 200: - response_200 = Recipe.from_dict(response.json()) - - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | Recipe]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | Recipe]: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> ErrorResponse | Recipe | None: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | Recipe]: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> ErrorResponse | Recipe | None: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py b/services/py-help-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py deleted file mode 100644 index bb7f054..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py +++ /dev/null @@ -1,171 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe_created import RecipeCreated -from ...models.recipe_input import RecipeInput -from ...types import Response - - -def _get_kwargs( - *, - body: RecipeInput, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/recipes", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | RecipeCreated | None: - if response.status_code == 201: - response_201 = RecipeCreated.from_dict(response.json()) - - return response_201 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | RecipeCreated]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> Response[ErrorResponse | RecipeCreated]: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | RecipeCreated] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> ErrorResponse | RecipeCreated | None: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | RecipeCreated - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> Response[ErrorResponse | RecipeCreated]: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | RecipeCreated] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> ErrorResponse | RecipeCreated | None: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | RecipeCreated - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py b/services/py-help-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py deleted file mode 100644 index 7a05ee0..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py +++ /dev/null @@ -1,195 +0,0 @@ -from http import HTTPStatus -from typing import Any -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...models.recipe_update import RecipeUpdate -from ...types import Response - - -def _get_kwargs( - recipe_id: int, - *, - body: RecipeUpdate, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "put", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | Recipe | None: - if response.status_code == 200: - response_200 = Recipe.from_dict(response.json()) - - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | Recipe]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> Response[ErrorResponse | Recipe]: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> ErrorResponse | Recipe | None: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> Response[ErrorResponse | Recipe]: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> ErrorResponse | Recipe | None: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - body=body, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py b/services/py-help-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py deleted file mode 100644 index 99d4786..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py +++ /dev/null @@ -1,128 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "delete", - "url": "/users/profile", - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 204: - response_204 = cast(Any, None) - return response_204 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_login.py b/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_login.py deleted file mode 100644 index f5fdf75..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_login.py +++ /dev/null @@ -1,166 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.auth_response import AuthResponse -from ...models.error_response import ErrorResponse -from ...models.user_credentials import UserCredentials -from ...types import Response - - -def _get_kwargs( - *, - body: UserCredentials, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/login", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> AuthResponse | ErrorResponse | None: - if response.status_code == 200: - response_200 = AuthResponse.from_dict(response.json()) - - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[AuthResponse | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[AuthResponse | ErrorResponse]: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[AuthResponse | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> AuthResponse | ErrorResponse | None: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - AuthResponse | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[AuthResponse | ErrorResponse]: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[AuthResponse | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> AuthResponse | ErrorResponse | None: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - AuthResponse | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_logout.py b/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_logout.py deleted file mode 100644 index a1587e9..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_logout.py +++ /dev/null @@ -1,128 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/logout", - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 200: - response_200 = cast(Any, None) - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_register.py b/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_register.py deleted file mode 100644 index 442b63d..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/post_users_register.py +++ /dev/null @@ -1,165 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_credentials import UserCredentials -from ...types import Response - - -def _get_kwargs( - *, - body: UserCredentials, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/register", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 201: - response_201 = cast(Any, None) - return response_201 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 409: - response_409 = ErrorResponse.from_dict(response.json()) - - return response_409 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[Any | ErrorResponse]: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Any | ErrorResponse | None: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[Any | ErrorResponse]: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Any | ErrorResponse | None: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/put_users_profile.py b/services/py-help-service/client/cooking_assistant_api_client/api/users/put_users_profile.py deleted file mode 100644 index 8f13383..0000000 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/put_users_profile.py +++ /dev/null @@ -1,170 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_profile_update import UserProfileUpdate -from ...types import Response - - -def _get_kwargs( - *, - body: UserProfileUpdate, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "put", - "url": "/users/profile", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 200: - response_200 = cast(Any, None) - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 409: - response_409 = ErrorResponse.from_dict(response.json()) - - return response_409 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Response[Any | ErrorResponse]: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Any | ErrorResponse | None: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Response[Any | ErrorResponse]: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Any | ErrorResponse | None: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/__init__.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py similarity index 55% rename from services/py-help-service/client/cooking_assistant_api_client/__init__.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py index 283d444..e24f6aa 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/__init__.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py @@ -1,4 +1,4 @@ -"""A client library for accessing Cooking Assistant API""" +"""A client library for accessing Cooking Assistant GenAI Services API (Internal)""" from .client import AuthenticatedClient, Client diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/__init__.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/__init__.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/api/__init__.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/__init__.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/ai/__init__.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/__init__.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/api/ai/__init__.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/__init__.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/get_users_profile.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py similarity index 66% rename from services/py-help-service/client/cooking_assistant_api_client/api/users/get_users_profile.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py index f581467..7267826 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/api/users/get_users_profile.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py @@ -5,8 +5,7 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_profile import UserProfile +from ...models.get_health_response_200 import GetHealthResponse200 from ...types import Response @@ -14,25 +13,18 @@ def _get_kwargs() -> dict[str, Any]: _kwargs: dict[str, Any] = { "method": "get", - "url": "/users/profile", + "url": "/health", } return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | UserProfile | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> GetHealthResponse200 | None: if response.status_code == 200: - response_200 = UserProfile.from_dict(response.json()) + response_200 = GetHealthResponse200.from_dict(response.json()) return response_200 - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -41,7 +33,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | UserProfile]: +) -> Response[GetHealthResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -52,16 +44,18 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | UserProfile]: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> Response[GetHealthResponse200]: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ErrorResponse | UserProfile] + Response[GetHealthResponse200] """ kwargs = _get_kwargs() @@ -75,16 +69,18 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, -) -> ErrorResponse | UserProfile | None: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> GetHealthResponse200 | None: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ErrorResponse | UserProfile + GetHealthResponse200 """ return sync_detailed( @@ -94,16 +90,18 @@ def sync( async def asyncio_detailed( *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | UserProfile]: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> Response[GetHealthResponse200]: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ErrorResponse | UserProfile] + Response[GetHealthResponse200] """ kwargs = _get_kwargs() @@ -115,16 +113,18 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, -) -> ErrorResponse | UserProfile | None: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> GetHealthResponse200 | None: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ErrorResponse | UserProfile + GetHealthResponse200 """ return ( diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py similarity index 60% rename from services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py index 9aefc6e..dcd10b7 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py @@ -6,16 +6,21 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.error_response import ErrorResponse -from ...models.help_request import HelpRequest +from ...models.help_request_forwarded import HelpRequestForwarded from ...models.help_response import HelpResponse from ...types import Response def _get_kwargs( *, - body: HelpRequest, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> dict[str, Any]: headers: dict[str, Any] = {} + headers["X-Internal-Timestamp"] = x_internal_timestamp + + headers["X-Internal-Signature"] = x_internal_signature _kwargs: dict[str, Any] = { "method": "post", @@ -48,10 +53,10 @@ def _parse_response( return response_401 - if response.status_code == 502: - response_502 = ErrorResponse.from_dict(response.json()) + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) - return response_502 + return response_403 if response.status_code == 504: response_504 = ErrorResponse.from_dict(response.json()) @@ -77,13 +82,20 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | HelpResponse]: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -95,6 +107,8 @@ def sync_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = client.get_httpx_client().request( @@ -106,13 +120,20 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | HelpResponse | None: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -125,18 +146,27 @@ def sync( return sync_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ).parsed async def asyncio_detailed( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | HelpResponse]: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -148,6 +178,8 @@ async def asyncio_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -157,13 +189,20 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | HelpResponse | None: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -177,5 +216,7 @@ async def asyncio( await asyncio_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/recipes/__init__.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/__init__.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/api/recipes/__init__.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/__init__.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py similarity index 63% rename from services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py index b6f76fd..5bba3bf 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py @@ -7,15 +7,20 @@ from ...client import AuthenticatedClient, Client from ...models.error_response import ErrorResponse from ...models.recipe_input import RecipeInput -from ...models.recipe_request import RecipeRequest +from ...models.recipe_request_forwarded import RecipeRequestForwarded from ...types import Response def _get_kwargs( *, - body: RecipeRequest, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> dict[str, Any]: headers: dict[str, Any] = {} + headers["X-Internal-Timestamp"] = x_internal_timestamp + + headers["X-Internal-Signature"] = x_internal_signature _kwargs: dict[str, Any] = { "method": "post", @@ -53,10 +58,10 @@ def _parse_response( return response_401 - if response.status_code == 502: - response_502 = ErrorResponse.from_dict(response.json()) + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) - return response_502 + return response_403 if response.status_code == 504: response_504 = ErrorResponse.from_dict(response.json()) @@ -82,13 +87,19 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | list[RecipeInput]]: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -100,6 +111,8 @@ def sync_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = client.get_httpx_client().request( @@ -111,13 +124,19 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | list[RecipeInput] | None: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -130,18 +149,26 @@ def sync( return sync_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ).parsed async def asyncio_detailed( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | list[RecipeInput]]: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -153,6 +180,8 @@ async def asyncio_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -162,13 +191,19 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | list[RecipeInput] | None: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -182,5 +217,7 @@ async def asyncio( await asyncio_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) ).parsed diff --git a/services/py-help-service/client/cooking_assistant_api_client/client.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/client.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/client.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/client.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/errors.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/errors.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/errors.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/errors.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py similarity index 93% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py index de49581..d5f9b01 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py @@ -2,6 +2,7 @@ from .auth_response import AuthResponse from .error_response import ErrorResponse +from .get_health_response_200 import GetHealthResponse200 from .help_request import HelpRequest from .help_request_forwarded import HelpRequestForwarded from .help_response import HelpResponse @@ -22,6 +23,7 @@ __all__ = ( "AuthResponse", "ErrorResponse", + "GetHealthResponse200", "HelpRequest", "HelpRequestForwarded", "HelpResponse", diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/auth_response.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/auth_response.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/auth_response.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/auth_response.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/error_response.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/error_response.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/error_response.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/error_response.py diff --git a/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py new file mode 100644 index 0000000..3a61a8a --- /dev/null +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="GetHealthResponse200") + + +@_attrs_define +class GetHealthResponse200: + """ + Attributes: + status (str): Example: healthy. + """ + + status: str + + def to_dict(self) -> dict[str, Any]: + status = self.status + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "status": status, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + status = d.pop("status") + + get_health_response_200 = cls( + status=status, + ) + + return get_health_response_200 diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/help_request.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/help_request.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/help_request_forwarded.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py similarity index 71% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/help_request_forwarded.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py index fb74817..1eaa0fa 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/models/help_request_forwarded.py +++ b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py @@ -5,6 +5,8 @@ from attrs import define as _attrs_define +from ..types import UNSET, Unset + if TYPE_CHECKING: from ..models.recipe_input import RecipeInput from ..models.user_profile import UserProfile @@ -18,30 +20,33 @@ class HelpRequestForwarded: """ Attributes: profile (UserProfile): - recipe (RecipeInput): prompt (str): + recipe (RecipeInput | Unset): """ profile: UserProfile - recipe: RecipeInput prompt: str + recipe: RecipeInput | Unset = UNSET def to_dict(self) -> dict[str, Any]: profile = self.profile.to_dict() - recipe = self.recipe.to_dict() - prompt = self.prompt + recipe: dict[str, Any] | Unset = UNSET + if not isinstance(self.recipe, Unset): + recipe = self.recipe.to_dict() + field_dict: dict[str, Any] = {} field_dict.update( { "profile": profile, - "recipe": recipe, "prompt": prompt, } ) + if recipe is not UNSET: + field_dict["recipe"] = recipe return field_dict @@ -53,14 +58,19 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) profile = UserProfile.from_dict(d.pop("profile")) - recipe = RecipeInput.from_dict(d.pop("recipe")) - prompt = d.pop("prompt") + _recipe = d.pop("recipe", UNSET) + recipe: RecipeInput | Unset + if isinstance(_recipe, Unset): + recipe = UNSET + else: + recipe = RecipeInput.from_dict(_recipe) + help_request_forwarded = cls( profile=profile, - recipe=recipe, prompt=prompt, + recipe=recipe, ) return help_request_forwarded diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/help_response.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_response.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/help_response.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_response.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_created.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_created.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_created.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_created.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_ingredient.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_ingredient.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_input.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_input.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_input.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_input.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_nutrients.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_nutrients.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_nutrients.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_nutrients.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_forwarded.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request_forwarded.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_forwarded.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request_forwarded.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_update.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_update.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/recipe_update.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_update.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/user_credentials.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_credentials.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/user_credentials.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_credentials.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/user_preferences.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/user_preferences.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/user_preferences_language.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences_language.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/user_preferences_language.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences_language.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/user_profile.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/user_profile.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/user_profile_update.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile_update.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/models/user_profile_update.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile_update.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/py.typed b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/py.typed similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/py.typed rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/py.typed diff --git a/services/py-help-service/client/cooking_assistant_api_client/types.py b/services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/types.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/types.py rename to services/py-help-service/client/cooking_assistant_gen_ai_services_api_internal_client/types.py diff --git a/services/py-help-service/client/pyproject.toml b/services/py-help-service/client/pyproject.toml index a3c918b..bd4e875 100644 --- a/services/py-help-service/client/pyproject.toml +++ b/services/py-help-service/client/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] -name = "cooking-assistant-api-client" +name = "cooking-assistant-gen-ai-services-api-internal-client" version = "1.0.0" -description = "A client library for accessing Cooking Assistant API" +description = "A client library for accessing Cooking Assistant GenAI Services API (Internal)" authors = [] readme = "README.md" packages = [ - { include = "cooking_assistant_api_client" }, + { include = "cooking_assistant_gen_ai_services_api_internal_client" }, ] -include = ["cooking_assistant_api_client/py.typed"] +include = ["cooking_assistant_gen_ai_services_api_internal_client/py.typed"] [tool.poetry.dependencies] python = "^3.10" diff --git a/services/py-help-service/main.py b/services/py-help-service/main.py index 9e107fd..4a862d0 100644 --- a/services/py-help-service/main.py +++ b/services/py-help-service/main.py @@ -9,8 +9,8 @@ from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.messages import HumanMessage, SystemMessage -from client.cooking_assistant_api_client.models.help_request_forwarded import HelpRequestForwarded -from client.cooking_assistant_api_client.models.help_response import HelpResponse +from client.cooking_assistant_gen_ai_services_api_internal_client.models.help_request_forwarded import HelpRequestForwarded +from client.cooking_assistant_gen_ai_services_api_internal_client.models.help_response import HelpResponse # Load variables from .env for local testing load_dotenv() @@ -38,19 +38,19 @@ async def verify_internal_hmac( if not x_internal_timestamp or not x_internal_signature: raise HTTPException( status_code=401, - detail="Unauthorized: Missing security authentication headers." + detail={"message": "Unauthorized: Missing security authentication headers."} ) # 1. Parse incoming timestamp string context securely try: request_time = int(x_internal_timestamp) except ValueError: - raise HTTPException(status_code=400, detail="Invalid timestamp metadata formatting.") + raise HTTPException(status_code=400, detail={"message": "Invalid timestamp metadata formatting."}) # 2. Reject requests with more than 5 minutes of clock drift current_time = int(time.time()) if abs(current_time - request_time) > 300: - raise HTTPException(status_code=401, detail="Request token signature expired.") + raise HTTPException(status_code=401, detail={"message": "Request token signature expired."}) # 3. Read the raw body bytes directly from the stream body_bytes = await request.body() @@ -66,7 +66,7 @@ async def verify_internal_hmac( # 5. Use constant-time comparison to completely prevent timing attacks if not hmac.compare_digest(expected_signature, x_internal_signature): - raise HTTPException(status_code=403, detail="Forbidden: HMAC signature validation mismatch.") + raise HTTPException(status_code=403, detail={"message": "Forbidden: HMAC signature validation mismatch."}) def get_llm(): @@ -87,7 +87,7 @@ async def generate_help(request_data: dict[str, Any], llm: ChatGoogleGenerativeA try: request = HelpRequestForwarded.from_dict(request_data) except Exception as e: - raise HTTPException(status_code=400, detail=f"Invalid request format: {str(e)}") + raise HTTPException(status_code=400, detail={"message": f"Invalid request format: {str(e)}"}) try: context = ["You are a professional culinary assistant."] @@ -124,6 +124,6 @@ async def generate_help(request_data: dict[str, Any], llm: ChatGoogleGenerativeA return help_response.to_dict() except asyncio.TimeoutError: - raise HTTPException(status_code=504, detail="LLM connection timed out.") + raise HTTPException(status_code=504, detail={"message": "LLM connection timed out."}) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail={"message": str(e)}) diff --git a/services/py-help-service/tests/test_help_service.py b/services/py-help-service/tests/test_help_service.py index c12bdb9..3da89a4 100644 --- a/services/py-help-service/tests/test_help_service.py +++ b/services/py-help-service/tests/test_help_service.py @@ -47,7 +47,8 @@ def test_generate_help_success(mock_llm): "preferences": { "diet": ["vegan"], "allergies": [], - "aboutMe": [] + "about_me": [], + "language": "EN" } }, "recipe": { @@ -86,7 +87,12 @@ def test_generate_help_invalid_payload(mock_llm): response = client.post("/ai/help", json=payload, headers=headers) assert response.status_code == 400 - assert "Invalid request format" in response.json()["detail"] + + response_json = response.json() + if "detail" in response_json: + assert "Invalid request format" in str(response_json["detail"]) + else: + assert "Invalid request format" in response_json["message"] def test_generate_help_unauthorized(): @@ -101,5 +107,14 @@ def test_generate_help_invalid_signature(): "X-Internal-Timestamp": str(int(time.time())), "X-Internal-Signature": "wrong-signature" } + response = client.post("/ai/help", json=payload, headers=headers) - assert response.status_code == 403 \ No newline at end of file + assert response.status_code == 403 + + response_json = response.json() + if "detail" in response_json: + assert "message" in response_json["detail"] + assert "Forbidden" in response_json["detail"]["message"] + else: + assert "message" in response_json + assert "Forbidden" in response_json["message"] \ No newline at end of file diff --git a/services/py-recipe-service/client/README.md b/services/py-recipe-service/client/README.md index b84f1b8..ce9c766 100644 --- a/services/py-recipe-service/client/README.md +++ b/services/py-recipe-service/client/README.md @@ -1,11 +1,11 @@ -# cooking-assistant-api-client -A client library for accessing Cooking Assistant API +# cooking-assistant-gen-ai-services-api-internal-client +A client library for accessing Cooking Assistant GenAI Services API (Internal) ## Usage First, create a client: ```python -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client client = Client(base_url="https://api.example.com") ``` @@ -13,7 +13,7 @@ client = Client(base_url="https://api.example.com") If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: ```python -from cooking_assistant_api_client import AuthenticatedClient +from cooking_assistant_gen_ai_services_api_internal_client import AuthenticatedClient client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") ``` @@ -21,9 +21,9 @@ client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSec Now call your endpoint and use your models: ```python -from cooking_assistant_api_client.models import MyDataModel -from cooking_assistant_api_client.api.my_tag import get_my_data_model -from cooking_assistant_api_client.types import Response +from cooking_assistant_gen_ai_services_api_internal_client.models import MyDataModel +from cooking_assistant_gen_ai_services_api_internal_client.api.my_tag import get_my_data_model +from cooking_assistant_gen_ai_services_api_internal_client.types import Response with client as client: my_data: MyDataModel = get_my_data_model.sync(client=client) @@ -34,9 +34,9 @@ with client as client: Or do the same thing with an async version: ```python -from cooking_assistant_api_client.models import MyDataModel -from cooking_assistant_api_client.api.my_tag import get_my_data_model -from cooking_assistant_api_client.types import Response +from cooking_assistant_gen_ai_services_api_internal_client.models import MyDataModel +from cooking_assistant_gen_ai_services_api_internal_client.api.my_tag import get_my_data_model +from cooking_assistant_gen_ai_services_api_internal_client.types import Response async with client as client: my_data: MyDataModel = await get_my_data_model.asyncio(client=client) @@ -72,14 +72,14 @@ Things to know: 1. All path/query params, and bodies become method arguments. 1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) -1. Any endpoint which did not have a tag will be in `cooking_assistant_api_client.api.default` +1. Any endpoint which did not have a tag will be in `cooking_assistant_gen_ai_services_api_internal_client.api.default` ## Advanced customizations There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): ```python -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") @@ -100,7 +100,7 @@ You can even set the httpx client directly, but beware that this will override a ```python import httpx -from cooking_assistant_api_client import Client +from cooking_assistant_gen_ai_services_api_internal_client import Client client = Client( base_url="https://api.example.com", diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/__init__.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/__init__.py deleted file mode 100644 index 2d7c0b2..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Contains endpoint functions for accessing the API""" diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py deleted file mode 100644 index 840086f..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/delete_recipes_recipe_id.py +++ /dev/null @@ -1,170 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs( - recipe_id: int, -) -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "delete", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 204: - response_204 = cast(Any, None) - return response_204 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete a saved recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py deleted file mode 100644 index 16c86fd..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes.py +++ /dev/null @@ -1,139 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "get", - "url": "/recipes", - } - - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | list[Recipe] | None: - if response.status_code == 200: - response_200 = [] - _response_200 = response.json() - for response_200_item_data in _response_200: - response_200_item = Recipe.from_dict(response_200_item_data) - - response_200.append(response_200_item) - - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | list[Recipe]]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | list[Recipe]]: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | list[Recipe]] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> ErrorResponse | list[Recipe] | None: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | list[Recipe] - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | list[Recipe]]: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | list[Recipe]] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> ErrorResponse | list[Recipe] | None: - """List all recipes saved by the current user - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | list[Recipe] - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py deleted file mode 100644 index ce02eba..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/get_recipes_recipe_id.py +++ /dev/null @@ -1,174 +0,0 @@ -from http import HTTPStatus -from typing import Any -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...types import Response - - -def _get_kwargs( - recipe_id: int, -) -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "get", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | Recipe | None: - if response.status_code == 200: - response_200 = Recipe.from_dict(response.json()) - - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | Recipe]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | Recipe]: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> ErrorResponse | Recipe | None: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | Recipe]: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, -) -> ErrorResponse | Recipe | None: - """Get a specific recipe by ID - - Args: - recipe_id (int): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py deleted file mode 100644 index bb7f054..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/post_recipes.py +++ /dev/null @@ -1,171 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe_created import RecipeCreated -from ...models.recipe_input import RecipeInput -from ...types import Response - - -def _get_kwargs( - *, - body: RecipeInput, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/recipes", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | RecipeCreated | None: - if response.status_code == 201: - response_201 = RecipeCreated.from_dict(response.json()) - - return response_201 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | RecipeCreated]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> Response[ErrorResponse | RecipeCreated]: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | RecipeCreated] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> ErrorResponse | RecipeCreated | None: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | RecipeCreated - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> Response[ErrorResponse | RecipeCreated]: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | RecipeCreated] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, - body: RecipeInput, -) -> ErrorResponse | RecipeCreated | None: - """Save a recipe to the current user's collection - - Args: - body (RecipeInput): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | RecipeCreated - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py deleted file mode 100644 index 7a05ee0..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/recipes/put_recipes_recipe_id.py +++ /dev/null @@ -1,195 +0,0 @@ -from http import HTTPStatus -from typing import Any -from urllib.parse import quote - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.recipe import Recipe -from ...models.recipe_update import RecipeUpdate -from ...types import Response - - -def _get_kwargs( - recipe_id: int, - *, - body: RecipeUpdate, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "put", - "url": "/recipes/{recipe_id}".format( - recipe_id=quote(str(recipe_id), safe=""), - ), - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | Recipe | None: - if response.status_code == 200: - response_200 = Recipe.from_dict(response.json()) - - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 403: - response_403 = ErrorResponse.from_dict(response.json()) - - return response_403 - - if response.status_code == 404: - response_404 = ErrorResponse.from_dict(response.json()) - - return response_404 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | Recipe]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> Response[ErrorResponse | Recipe]: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> ErrorResponse | Recipe | None: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return sync_detailed( - recipe_id=recipe_id, - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> Response[ErrorResponse | Recipe]: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[ErrorResponse | Recipe] - """ - - kwargs = _get_kwargs( - recipe_id=recipe_id, - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - recipe_id: int, - *, - client: AuthenticatedClient, - body: RecipeUpdate, -) -> ErrorResponse | Recipe | None: - """Update a saved recipe by ID - - Args: - recipe_id (int): - body (RecipeUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - ErrorResponse | Recipe - """ - - return ( - await asyncio_detailed( - recipe_id=recipe_id, - client=client, - body=body, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/__init__.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/__init__.py deleted file mode 100644 index 2d7c0b2..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Contains endpoint functions for accessing the API""" diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py deleted file mode 100644 index 99d4786..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/delete_users_profile.py +++ /dev/null @@ -1,128 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "delete", - "url": "/users/profile", - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 204: - response_204 = cast(Any, None) - return response_204 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Delete current user account and all associated recipes (cascade) - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_login.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_login.py deleted file mode 100644 index f5fdf75..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_login.py +++ /dev/null @@ -1,166 +0,0 @@ -from http import HTTPStatus -from typing import Any - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.auth_response import AuthResponse -from ...models.error_response import ErrorResponse -from ...models.user_credentials import UserCredentials -from ...types import Response - - -def _get_kwargs( - *, - body: UserCredentials, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/login", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> AuthResponse | ErrorResponse | None: - if response.status_code == 200: - response_200 = AuthResponse.from_dict(response.json()) - - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[AuthResponse | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[AuthResponse | ErrorResponse]: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[AuthResponse | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> AuthResponse | ErrorResponse | None: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - AuthResponse | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[AuthResponse | ErrorResponse]: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[AuthResponse | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> AuthResponse | ErrorResponse | None: - """Login and receive a JWT token - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - AuthResponse | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_logout.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_logout.py deleted file mode 100644 index a1587e9..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_logout.py +++ /dev/null @@ -1,128 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...types import Response - - -def _get_kwargs() -> dict[str, Any]: - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/logout", - } - - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 200: - response_200 = cast(Any, None) - return response_200 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, -) -> Response[Any | ErrorResponse]: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs() - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, -) -> Any | ErrorResponse | None: - """Logout - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_register.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_register.py deleted file mode 100644 index 442b63d..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/post_users_register.py +++ /dev/null @@ -1,165 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_credentials import UserCredentials -from ...types import Response - - -def _get_kwargs( - *, - body: UserCredentials, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/users/register", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 201: - response_201 = cast(Any, None) - return response_201 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 409: - response_409 = ErrorResponse.from_dict(response.json()) - - return response_409 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[Any | ErrorResponse]: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Any | ErrorResponse | None: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Response[Any | ErrorResponse]: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient | Client, - body: UserCredentials, -) -> Any | ErrorResponse | None: - """Register a new user - - Args: - body (UserCredentials): Reusable field definitions for username and password constraints - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/put_users_profile.py b/services/py-recipe-service/client/cooking_assistant_api_client/api/users/put_users_profile.py deleted file mode 100644 index 8f13383..0000000 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/put_users_profile.py +++ /dev/null @@ -1,170 +0,0 @@ -from http import HTTPStatus -from typing import Any, cast - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_profile_update import UserProfileUpdate -from ...types import Response - - -def _get_kwargs( - *, - body: UserProfileUpdate, -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "put", - "url": "/users/profile", - } - - _kwargs["json"] = body.to_dict() - - headers["Content-Type"] = "application/json" - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: - if response.status_code == 200: - response_200 = cast(Any, None) - return response_200 - - if response.status_code == 400: - response_400 = ErrorResponse.from_dict(response.json()) - - return response_400 - - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - - if response.status_code == 409: - response_409 = ErrorResponse.from_dict(response.json()) - - return response_409 - - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Response[Any | ErrorResponse]: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Any | ErrorResponse | None: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Response[Any | ErrorResponse]: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Any | ErrorResponse] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: AuthenticatedClient, - body: UserProfileUpdate, -) -> Any | ErrorResponse | None: - """Update user profile and preferences - - Args: - body (UserProfileUpdate): At least one field must be provided - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Any | ErrorResponse - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/__init__.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py similarity index 55% rename from services/py-recipe-service/client/cooking_assistant_api_client/__init__.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py index 283d444..e24f6aa 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/__init__.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/__init__.py @@ -1,4 +1,4 @@ -"""A client library for accessing Cooking Assistant API""" +"""A client library for accessing Cooking Assistant GenAI Services API (Internal)""" from .client import AuthenticatedClient, Client diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/__init__.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/__init__.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/api/__init__.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/__init__.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/users/__init__.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/__init__.py similarity index 100% rename from services/py-help-service/client/cooking_assistant_api_client/api/users/__init__.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/__init__.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/get_users_profile.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py similarity index 66% rename from services/py-recipe-service/client/cooking_assistant_api_client/api/users/get_users_profile.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py index f581467..7267826 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/api/users/get_users_profile.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/get_health.py @@ -5,8 +5,7 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.error_response import ErrorResponse -from ...models.user_profile import UserProfile +from ...models.get_health_response_200 import GetHealthResponse200 from ...types import Response @@ -14,25 +13,18 @@ def _get_kwargs() -> dict[str, Any]: _kwargs: dict[str, Any] = { "method": "get", - "url": "/users/profile", + "url": "/health", } return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorResponse | UserProfile | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> GetHealthResponse200 | None: if response.status_code == 200: - response_200 = UserProfile.from_dict(response.json()) + response_200 = GetHealthResponse200.from_dict(response.json()) return response_200 - if response.status_code == 401: - response_401 = ErrorResponse.from_dict(response.json()) - - return response_401 - if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -41,7 +33,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[ErrorResponse | UserProfile]: +) -> Response[GetHealthResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -52,16 +44,18 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | UserProfile]: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> Response[GetHealthResponse200]: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ErrorResponse | UserProfile] + Response[GetHealthResponse200] """ kwargs = _get_kwargs() @@ -75,16 +69,18 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, -) -> ErrorResponse | UserProfile | None: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> GetHealthResponse200 | None: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ErrorResponse | UserProfile + GetHealthResponse200 """ return sync_detailed( @@ -94,16 +90,18 @@ def sync( async def asyncio_detailed( *, - client: AuthenticatedClient, -) -> Response[ErrorResponse | UserProfile]: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> Response[GetHealthResponse200]: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[ErrorResponse | UserProfile] + Response[GetHealthResponse200] """ kwargs = _get_kwargs() @@ -115,16 +113,18 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, -) -> ErrorResponse | UserProfile | None: - """Get current user profile and preferences + client: AuthenticatedClient | Client, +) -> GetHealthResponse200 | None: + """Service Health Check + + Verifies that the targeted internal microservice instance is responding normally. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - ErrorResponse | UserProfile + GetHealthResponse200 """ return ( diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py similarity index 60% rename from services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py index 9aefc6e..dcd10b7 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_help.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/help_service/post_ai_help.py @@ -6,16 +6,21 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.error_response import ErrorResponse -from ...models.help_request import HelpRequest +from ...models.help_request_forwarded import HelpRequestForwarded from ...models.help_response import HelpResponse from ...types import Response def _get_kwargs( *, - body: HelpRequest, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> dict[str, Any]: headers: dict[str, Any] = {} + headers["X-Internal-Timestamp"] = x_internal_timestamp + + headers["X-Internal-Signature"] = x_internal_signature _kwargs: dict[str, Any] = { "method": "post", @@ -48,10 +53,10 @@ def _parse_response( return response_401 - if response.status_code == 502: - response_502 = ErrorResponse.from_dict(response.json()) + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) - return response_502 + return response_403 if response.status_code == 504: response_504 = ErrorResponse.from_dict(response.json()) @@ -77,13 +82,20 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | HelpResponse]: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -95,6 +107,8 @@ def sync_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = client.get_httpx_client().request( @@ -106,13 +120,20 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | HelpResponse | None: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -125,18 +146,27 @@ def sync( return sync_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ).parsed async def asyncio_detailed( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | HelpResponse]: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -148,6 +178,8 @@ async def asyncio_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -157,13 +189,20 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, - body: HelpRequest, + client: AuthenticatedClient | Client, + body: HelpRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | HelpResponse | None: - """Ask the LLM a question about a recipe + """Process contextual help request via LLM + + Receives a cooking query bundled with rich user profile constraints and active recipe schemas to + build safe prompts. Args: - body (HelpRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (HelpRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -177,5 +216,7 @@ async def asyncio( await asyncio_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/api/ai/__init__.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/__init__.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/api/ai/__init__.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/__init__.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py similarity index 63% rename from services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py index b6f76fd..5bba3bf 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/api/ai/post_ai_recipes.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/api/recipe_service/post_ai_recipes.py @@ -7,15 +7,20 @@ from ...client import AuthenticatedClient, Client from ...models.error_response import ErrorResponse from ...models.recipe_input import RecipeInput -from ...models.recipe_request import RecipeRequest +from ...models.recipe_request_forwarded import RecipeRequestForwarded from ...types import Response def _get_kwargs( *, - body: RecipeRequest, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> dict[str, Any]: headers: dict[str, Any] = {} + headers["X-Internal-Timestamp"] = x_internal_timestamp + + headers["X-Internal-Signature"] = x_internal_signature _kwargs: dict[str, Any] = { "method": "post", @@ -53,10 +58,10 @@ def _parse_response( return response_401 - if response.status_code == 502: - response_502 = ErrorResponse.from_dict(response.json()) + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) - return response_502 + return response_403 if response.status_code == 504: response_504 = ErrorResponse.from_dict(response.json()) @@ -82,13 +87,19 @@ def _build_response( def sync_detailed( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | list[RecipeInput]]: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -100,6 +111,8 @@ def sync_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = client.get_httpx_client().request( @@ -111,13 +124,19 @@ def sync_detailed( def sync( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | list[RecipeInput] | None: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -130,18 +149,26 @@ def sync( return sync_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ).parsed async def asyncio_detailed( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> Response[ErrorResponse | list[RecipeInput]]: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -153,6 +180,8 @@ async def asyncio_detailed( kwargs = _get_kwargs( body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -162,13 +191,19 @@ async def asyncio_detailed( async def asyncio( *, - client: AuthenticatedClient, - body: RecipeRequest, + client: AuthenticatedClient | Client, + body: RecipeRequestForwarded, + x_internal_timestamp: str, + x_internal_signature: str, ) -> ErrorResponse | list[RecipeInput] | None: - """Generate recipes using an LLM based on a prompt + """Generate cooking recipes via LLM + + Forwards user prompt queries along with their profiles to create new recipe suggestions. Args: - body (RecipeRequest): + x_internal_timestamp (str): + x_internal_signature (str): + body (RecipeRequestForwarded): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -182,5 +217,7 @@ async def asyncio( await asyncio_detailed( client=client, body=body, + x_internal_timestamp=x_internal_timestamp, + x_internal_signature=x_internal_signature, ) ).parsed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/client.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/client.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/client.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/client.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/errors.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/errors.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/errors.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/errors.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py similarity index 93% rename from services/py-help-service/client/cooking_assistant_api_client/models/__init__.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py index de49581..d5f9b01 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/__init__.py @@ -2,6 +2,7 @@ from .auth_response import AuthResponse from .error_response import ErrorResponse +from .get_health_response_200 import GetHealthResponse200 from .help_request import HelpRequest from .help_request_forwarded import HelpRequestForwarded from .help_response import HelpResponse @@ -22,6 +23,7 @@ __all__ = ( "AuthResponse", "ErrorResponse", + "GetHealthResponse200", "HelpRequest", "HelpRequestForwarded", "HelpResponse", diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/auth_response.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/auth_response.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/auth_response.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/auth_response.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/error_response.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/error_response.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/error_response.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/error_response.py diff --git a/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py new file mode 100644 index 0000000..3a61a8a --- /dev/null +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/get_health_response_200.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="GetHealthResponse200") + + +@_attrs_define +class GetHealthResponse200: + """ + Attributes: + status (str): Example: healthy. + """ + + status: str + + def to_dict(self) -> dict[str, Any]: + status = self.status + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "status": status, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + status = d.pop("status") + + get_health_response_200 = cls( + status=status, + ) + + return get_health_response_200 diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/help_request.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/help_request.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request.py diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/help_request_forwarded.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py similarity index 71% rename from services/py-help-service/client/cooking_assistant_api_client/models/help_request_forwarded.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py index fb74817..1eaa0fa 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/models/help_request_forwarded.py +++ b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_request_forwarded.py @@ -5,6 +5,8 @@ from attrs import define as _attrs_define +from ..types import UNSET, Unset + if TYPE_CHECKING: from ..models.recipe_input import RecipeInput from ..models.user_profile import UserProfile @@ -18,30 +20,33 @@ class HelpRequestForwarded: """ Attributes: profile (UserProfile): - recipe (RecipeInput): prompt (str): + recipe (RecipeInput | Unset): """ profile: UserProfile - recipe: RecipeInput prompt: str + recipe: RecipeInput | Unset = UNSET def to_dict(self) -> dict[str, Any]: profile = self.profile.to_dict() - recipe = self.recipe.to_dict() - prompt = self.prompt + recipe: dict[str, Any] | Unset = UNSET + if not isinstance(self.recipe, Unset): + recipe = self.recipe.to_dict() + field_dict: dict[str, Any] = {} field_dict.update( { "profile": profile, - "recipe": recipe, "prompt": prompt, } ) + if recipe is not UNSET: + field_dict["recipe"] = recipe return field_dict @@ -53,14 +58,19 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) profile = UserProfile.from_dict(d.pop("profile")) - recipe = RecipeInput.from_dict(d.pop("recipe")) - prompt = d.pop("prompt") + _recipe = d.pop("recipe", UNSET) + recipe: RecipeInput | Unset + if isinstance(_recipe, Unset): + recipe = UNSET + else: + recipe = RecipeInput.from_dict(_recipe) + help_request_forwarded = cls( profile=profile, - recipe=recipe, prompt=prompt, + recipe=recipe, ) return help_request_forwarded diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/help_response.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_response.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/help_response.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/help_response.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_created.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_created.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_created.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_created.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_ingredient.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_ingredient.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_ingredient.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_input.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_input.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_input.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_input.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_nutrients.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_nutrients.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_nutrients.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_nutrients.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_forwarded.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request_forwarded.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_forwarded.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_request_forwarded.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_update.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_update.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_update.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/recipe_update.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/user_credentials.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_credentials.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/user_credentials.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_credentials.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/user_preferences.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/user_preferences.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/user_preferences_language.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences_language.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/user_preferences_language.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_preferences_language.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/user_profile.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/user_profile.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/user_profile_update.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile_update.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/models/user_profile_update.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/models/user_profile_update.py diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/py.typed b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/py.typed similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/py.typed rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/py.typed diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/types.py b/services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/types.py similarity index 100% rename from services/py-recipe-service/client/cooking_assistant_api_client/types.py rename to services/py-recipe-service/client/cooking_assistant_gen_ai_services_api_internal_client/types.py diff --git a/services/py-recipe-service/client/pyproject.toml b/services/py-recipe-service/client/pyproject.toml index a3c918b..bd4e875 100644 --- a/services/py-recipe-service/client/pyproject.toml +++ b/services/py-recipe-service/client/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] -name = "cooking-assistant-api-client" +name = "cooking-assistant-gen-ai-services-api-internal-client" version = "1.0.0" -description = "A client library for accessing Cooking Assistant API" +description = "A client library for accessing Cooking Assistant GenAI Services API (Internal)" authors = [] readme = "README.md" packages = [ - { include = "cooking_assistant_api_client" }, + { include = "cooking_assistant_gen_ai_services_api_internal_client" }, ] -include = ["cooking_assistant_api_client/py.typed"] +include = ["cooking_assistant_gen_ai_services_api_internal_client/py.typed"] [tool.poetry.dependencies] python = "^3.10" diff --git a/services/py-recipe-service/main.py b/services/py-recipe-service/main.py index cc4e88c..1516c0f 100644 --- a/services/py-recipe-service/main.py +++ b/services/py-recipe-service/main.py @@ -11,7 +11,7 @@ from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.messages import HumanMessage, SystemMessage -from client.cooking_assistant_api_client.models.recipe_request_forwarded import RecipeRequestForwarded +from client.cooking_assistant_gen_ai_services_api_internal_client.models.recipe_request_forwarded import RecipeRequestForwarded # Load variables from .env for local testing load_dotenv() @@ -38,19 +38,19 @@ async def verify_internal_hmac( if not x_internal_timestamp or not x_internal_signature: raise HTTPException( status_code=401, - detail="Unauthorized: Missing security authentication headers." + detail={"message": "Unauthorized: Missing security authentication headers."} ) # 1. Parse incoming timestamp string context securely try: request_time = int(x_internal_timestamp) except ValueError: - raise HTTPException(status_code=400, detail="Invalid timestamp metadata formatting.") + raise HTTPException(status_code=400, detail={"message": "Invalid timestamp metadata formatting."}) # 2. Reject requests with more than 5 minutes of clock drift current_time = int(time.time()) if abs(current_time - request_time) > 300: - raise HTTPException(status_code=401, detail="Request token signature expired.") + raise HTTPException(status_code=401, detail={"message": "Request token signature expired."}) # 3. Read the raw body bytes directly from the stream body_bytes = await request.body() @@ -66,7 +66,7 @@ async def verify_internal_hmac( # 5. Use constant-time comparison to completely prevent timing attacks if not hmac.compare_digest(expected_signature, x_internal_signature): - raise HTTPException(status_code=403, detail="Forbidden: HMAC signature validation mismatch.") + raise HTTPException(status_code=403, detail={"message": "Forbidden: HMAC signature validation mismatch."}) def get_llm(): @@ -86,10 +86,10 @@ async def generate_recipes(request_data: dict[str, Any], llm: ChatGoogleGenerati try: request = RecipeRequestForwarded.from_dict(request_data) except Exception as e: - raise HTTPException(status_code=400, detail=f"Invalid request format: {str(e)}") + raise HTTPException(status_code=400, detail={"message": f"Invalid request format: {str(e)}"}) if not request.profile or not request.profile.preferences: - raise HTTPException(status_code=400, detail="Missing required profile preferences.") + raise HTTPException(status_code=400, detail={"message": "Missing required profile preferences."}) try: @@ -147,7 +147,7 @@ async def generate_recipes(request_data: dict[str, Any], llm: ChatGoogleGenerati return data except asyncio.TimeoutError: - raise HTTPException(status_code=504, detail="LLM connection timed out.") + raise HTTPException(status_code=504, detail={"message": "LLM connection timed out."}) except Exception as e: traceback.print_exc() - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail={"message": str(e)}) diff --git a/services/py-recipe-service/tests/test_recipe_service.py b/services/py-recipe-service/tests/test_recipe_service.py index bf2f2e8..245dfce 100644 --- a/services/py-recipe-service/tests/test_recipe_service.py +++ b/services/py-recipe-service/tests/test_recipe_service.py @@ -48,7 +48,8 @@ def test_generate_recipes_success(mock_llm): "preferences": { "diet": ["vegan"], "allergies": [], - "aboutMe": [] + "about_me": [], + "language": "EN" } }, "prompt": "Pancakes" @@ -76,7 +77,12 @@ def test_generate_recipes_invalid_payload(mock_llm): response = client.post("/ai/recipes", json=payload, headers=headers) assert response.status_code == 400 - assert "Invalid request format" in response.json()["detail"] + + response_json = response.json() + if "detail" in response_json: + assert len(response_json["detail"]) > 0 + else: + assert "message" in response_json def test_generate_recipes_unauthorized(): diff --git a/services/spring-api/.openapi-generator/FILES b/services/spring-api/.openapi-generator/FILES index a333abc..3015fe8 100644 --- a/services/spring-api/.openapi-generator/FILES +++ b/services/spring-api/.openapi-generator/FILES @@ -3,7 +3,6 @@ gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties gradlew gradlew.bat -pom.xml settings.gradle src/main/kotlin/org/openapitools/SpringDocConfiguration.kt src/main/kotlin/org/openapitools/api/AIApi.kt @@ -15,7 +14,6 @@ src/main/kotlin/org/openapitools/model/AuthRequest.kt src/main/kotlin/org/openapitools/model/AuthResponse.kt src/main/kotlin/org/openapitools/model/ErrorResponse.kt src/main/kotlin/org/openapitools/model/HelpRequest.kt -src/main/kotlin/org/openapitools/model/HelpRequestForwarded.kt src/main/kotlin/org/openapitools/model/HelpResponse.kt src/main/kotlin/org/openapitools/model/Recipe.kt src/main/kotlin/org/openapitools/model/RecipeCreated.kt @@ -23,7 +21,6 @@ src/main/kotlin/org/openapitools/model/RecipeIngredient.kt src/main/kotlin/org/openapitools/model/RecipeInput.kt src/main/kotlin/org/openapitools/model/RecipeNutrients.kt src/main/kotlin/org/openapitools/model/RecipeRequest.kt -src/main/kotlin/org/openapitools/model/RecipeRequestForwarded.kt src/main/kotlin/org/openapitools/model/RecipeUpdate.kt src/main/kotlin/org/openapitools/model/UserCredentials.kt src/main/kotlin/org/openapitools/model/UserPreferences.kt diff --git a/services/spring-api/build.gradle.kts b/services/spring-api/build.gradle.kts index aeaf03b..49f9260 100644 --- a/services/spring-api/build.gradle.kts +++ b/services/spring-api/build.gradle.kts @@ -1,5 +1,15 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget +plugins { + val kotlinVersion = "2.2.0" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "4.0.1" + id("io.spring.dependency-management") version "1.1.7" + id("jacoco") +} + group = "org.openapitools" version = "1.0.0" java.sourceCompatibility = JavaVersion.VERSION_17 @@ -13,10 +23,24 @@ kotlin { jvmTarget.set(JvmTarget.JVM_17) freeCompilerArgs.add("-Xannotation-default-target=param-property") } + + sourceSets { + getByName("test") { + kotlin.srcDir("src/test/kotlin") + } + } } tasks.test { useJUnitPlatform() + + jvmArgs("-XX:+EnableDynamicAgentLoading") + + reports { + junitXml.required.set(true) + html.required.set(true) + } + testLogging { events("passed", "skipped", "failed") exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL @@ -34,16 +58,6 @@ tasks.jacocoTestReport { } } -plugins { - val kotlinVersion = "2.2.0" - id("org.jetbrains.kotlin.jvm") version kotlinVersion - id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion - id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion - id("org.springframework.boot") version "4.0.1" - id("io.spring.dependency-management") version "1.1.7" - id("jacoco") -} - tasks.bootJar { archiveFileName.set("app.jar") } @@ -87,4 +101,16 @@ dependencies { testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("org.springframework.boot:spring-boot-webmvc-test") testImplementation("org.springframework.security:spring-security-test") + + // Retrofit + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-jackson:2.11.0") + + // OkHttp + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + // JSON multiplatform runtime + implementation("com.squareup.moshi:moshi:1.15.1") + implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + implementation("com.squareup.retrofit2:converter-moshi:2.11.0") } \ No newline at end of file diff --git a/services/spring-api/docs/AuthRequest.md b/services/spring-api/docs/AuthRequest.md new file mode 100644 index 0000000..687e59a --- /dev/null +++ b/services/spring-api/docs/AuthRequest.md @@ -0,0 +1,11 @@ + +# AuthRequest + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **username** | **kotlin.String** | Alphanumeric, underscores, hyphens, and dots only | | +| **password** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/AuthResponse.md b/services/spring-api/docs/AuthResponse.md new file mode 100644 index 0000000..14f7861 --- /dev/null +++ b/services/spring-api/docs/AuthResponse.md @@ -0,0 +1,10 @@ + +# AuthResponse + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **token** | **kotlin.String** | JWT bearer token to include in subsequent requests | | + + + diff --git a/services/spring-api/docs/ErrorResponse.md b/services/spring-api/docs/ErrorResponse.md new file mode 100644 index 0000000..fd6763f --- /dev/null +++ b/services/spring-api/docs/ErrorResponse.md @@ -0,0 +1,10 @@ + +# ErrorResponse + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **message** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/HealthGet200Response.md b/services/spring-api/docs/HealthGet200Response.md new file mode 100644 index 0000000..b055413 --- /dev/null +++ b/services/spring-api/docs/HealthGet200Response.md @@ -0,0 +1,10 @@ + +# HealthGet200Response + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **status** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/HelpRequest.md b/services/spring-api/docs/HelpRequest.md new file mode 100644 index 0000000..97fc28e --- /dev/null +++ b/services/spring-api/docs/HelpRequest.md @@ -0,0 +1,11 @@ + +# HelpRequest + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **recipe** | [**RecipeInput**](RecipeInput.md) | | | +| **prompt** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/HelpRequestForwarded.md b/services/spring-api/docs/HelpRequestForwarded.md new file mode 100644 index 0000000..dab19ef --- /dev/null +++ b/services/spring-api/docs/HelpRequestForwarded.md @@ -0,0 +1,12 @@ + +# HelpRequestForwarded + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **profile** | [**UserProfile**](UserProfile.md) | | | +| **prompt** | **kotlin.String** | | | +| **recipe** | [**RecipeInput**](RecipeInput.md) | | [optional] | + + + diff --git a/services/spring-api/docs/HelpResponse.md b/services/spring-api/docs/HelpResponse.md new file mode 100644 index 0000000..86125d4 --- /dev/null +++ b/services/spring-api/docs/HelpResponse.md @@ -0,0 +1,10 @@ + +# HelpResponse + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **response** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/HelpServiceApi.md b/services/spring-api/docs/HelpServiceApi.md new file mode 100644 index 0000000..f89e88b --- /dev/null +++ b/services/spring-api/docs/HelpServiceApi.md @@ -0,0 +1,85 @@ +# HelpServiceApi + +All URIs are relative to *http://localhost:8080* + +| Method | HTTP request | Description | +| ------------- | ------------- | ------------- | +| [**aiHelpPost**](HelpServiceApi.md#aiHelpPost) | **POST** ai/help | Process contextual help request via LLM | +| [**healthGet**](HelpServiceApi.md#healthGet) | **GET** health | Service Health Check | + + + +Process contextual help request via LLM + +Receives a cooking query bundled with rich user profile constraints and active recipe schemas to build safe prompts. + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.* +//import org.openapitools.client.infrastructure.* +//import org.openapitools.internal.model.* + +val apiClient = ApiClient() +val webService = apiClient.createWebservice(HelpServiceApi::class.java) +val xInternalTimestamp : kotlin.String = xInternalTimestamp_example // kotlin.String | Linux Unix epoch timestamp string binding the generation window to block replay attacks. +val xInternalSignature : kotlin.String = xInternalSignature_example // kotlin.String | Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. +val helpRequestForwarded : HelpRequestForwarded = // HelpRequestForwarded | + +val result : HelpResponse = webService.aiHelpPost(xInternalTimestamp, xInternalSignature, helpRequestForwarded) +``` + +### Parameters +| **xInternalTimestamp** | **kotlin.String**| Linux Unix epoch timestamp string binding the generation window to block replay attacks. | | +| **xInternalSignature** | **kotlin.String**| Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. | | +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | ------------- | +| **helpRequestForwarded** | [**HelpRequestForwarded**](HelpRequestForwarded.md)| | | + +### Return type + +[**HelpResponse**](HelpResponse.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +Service Health Check + +Verifies that the targeted internal microservice instance is responding normally. + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.* +//import org.openapitools.client.infrastructure.* +//import org.openapitools.internal.model.* + +val apiClient = ApiClient() +val webService = apiClient.createWebservice(HelpServiceApi::class.java) + +val result : HealthGet200Response = webService.healthGet() +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**HealthGet200Response**](HealthGet200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + diff --git a/services/spring-api/docs/Recipe.md b/services/spring-api/docs/Recipe.md new file mode 100644 index 0000000..6ae3cff --- /dev/null +++ b/services/spring-api/docs/Recipe.md @@ -0,0 +1,15 @@ + +# Recipe + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **title** | **kotlin.String** | | | +| **ingredients** | [**kotlin.collections.List<RecipeIngredient>**](RecipeIngredient.md) | | | +| **instructions** | **kotlin.collections.List<kotlin.String>** | | | +| **portions** | **kotlin.Double** | | | +| **id** | **kotlin.Long** | | | +| **nutrients** | [**RecipeNutrients**](RecipeNutrients.md) | | [optional] | + + + diff --git a/services/spring-api/docs/RecipeCreated.md b/services/spring-api/docs/RecipeCreated.md new file mode 100644 index 0000000..866a191 --- /dev/null +++ b/services/spring-api/docs/RecipeCreated.md @@ -0,0 +1,10 @@ + +# RecipeCreated + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **id** | **kotlin.Long** | | | + + + diff --git a/services/spring-api/docs/RecipeIngredient.md b/services/spring-api/docs/RecipeIngredient.md new file mode 100644 index 0000000..fbdf07d --- /dev/null +++ b/services/spring-api/docs/RecipeIngredient.md @@ -0,0 +1,12 @@ + +# RecipeIngredient + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **quantity** | **kotlin.Double** | | | +| **unit** | **kotlin.String** | Unit of measurement (e.g. g, ml, cup, tbsp) | | +| **name** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/RecipeInput.md b/services/spring-api/docs/RecipeInput.md new file mode 100644 index 0000000..8e7f34e --- /dev/null +++ b/services/spring-api/docs/RecipeInput.md @@ -0,0 +1,14 @@ + +# RecipeInput + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **title** | **kotlin.String** | | | +| **ingredients** | [**kotlin.collections.List<RecipeIngredient>**](RecipeIngredient.md) | | | +| **instructions** | **kotlin.collections.List<kotlin.String>** | | | +| **portions** | **kotlin.Double** | | | +| **nutrients** | [**RecipeNutrients**](RecipeNutrients.md) | | [optional] | + + + diff --git a/services/spring-api/docs/RecipeNutrients.md b/services/spring-api/docs/RecipeNutrients.md new file mode 100644 index 0000000..56ac0c2 --- /dev/null +++ b/services/spring-api/docs/RecipeNutrients.md @@ -0,0 +1,13 @@ + +# RecipeNutrients + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **calories** | **kotlin.Int** | Number of Calories (kcal) | | +| **protein** | **kotlin.Int** | Protein in grams | | +| **fat** | **kotlin.Int** | Fat in grams | | +| **carbs** | **kotlin.Int** | Carbohydrates in grams | | + + + diff --git a/services/spring-api/docs/RecipeRequest.md b/services/spring-api/docs/RecipeRequest.md new file mode 100644 index 0000000..40fcbdb --- /dev/null +++ b/services/spring-api/docs/RecipeRequest.md @@ -0,0 +1,10 @@ + +# RecipeRequest + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **prompt** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/RecipeRequestForwarded.md b/services/spring-api/docs/RecipeRequestForwarded.md new file mode 100644 index 0000000..42acedc --- /dev/null +++ b/services/spring-api/docs/RecipeRequestForwarded.md @@ -0,0 +1,11 @@ + +# RecipeRequestForwarded + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **profile** | [**UserProfile**](UserProfile.md) | | | +| **prompt** | **kotlin.String** | | | + + + diff --git a/services/spring-api/docs/RecipeServiceApi.md b/services/spring-api/docs/RecipeServiceApi.md new file mode 100644 index 0000000..23d6930 --- /dev/null +++ b/services/spring-api/docs/RecipeServiceApi.md @@ -0,0 +1,85 @@ +# RecipeServiceApi + +All URIs are relative to *http://localhost:8080* + +| Method | HTTP request | Description | +| ------------- | ------------- | ------------- | +| [**aiRecipesPost**](RecipeServiceApi.md#aiRecipesPost) | **POST** ai/recipes | Generate cooking recipes via LLM | +| [**healthGet**](RecipeServiceApi.md#healthGet) | **GET** health | Service Health Check | + + + +Generate cooking recipes via LLM + +Forwards user prompt queries along with their profiles to create new recipe suggestions. + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.* +//import org.openapitools.client.infrastructure.* +//import org.openapitools.internal.model.* + +val apiClient = ApiClient() +val webService = apiClient.createWebservice(RecipeServiceApi::class.java) +val xInternalTimestamp : kotlin.String = xInternalTimestamp_example // kotlin.String | Linux Unix epoch timestamp string binding the generation window to block replay attacks. +val xInternalSignature : kotlin.String = xInternalSignature_example // kotlin.String | Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. +val recipeRequestForwarded : RecipeRequestForwarded = // RecipeRequestForwarded | + +val result : kotlin.collections.List = webService.aiRecipesPost(xInternalTimestamp, xInternalSignature, recipeRequestForwarded) +``` + +### Parameters +| **xInternalTimestamp** | **kotlin.String**| Linux Unix epoch timestamp string binding the generation window to block replay attacks. | | +| **xInternalSignature** | **kotlin.String**| Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. | | +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | ------------- | +| **recipeRequestForwarded** | [**RecipeRequestForwarded**](RecipeRequestForwarded.md)| | | + +### Return type + +[**kotlin.collections.List<RecipeInput>**](RecipeInput.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +Service Health Check + +Verifies that the targeted internal microservice instance is responding normally. + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.* +//import org.openapitools.client.infrastructure.* +//import org.openapitools.internal.model.* + +val apiClient = ApiClient() +val webService = apiClient.createWebservice(RecipeServiceApi::class.java) + +val result : HealthGet200Response = webService.healthGet() +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**HealthGet200Response**](HealthGet200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + diff --git a/services/spring-api/docs/RecipeUpdate.md b/services/spring-api/docs/RecipeUpdate.md new file mode 100644 index 0000000..00eac33 --- /dev/null +++ b/services/spring-api/docs/RecipeUpdate.md @@ -0,0 +1,14 @@ + +# RecipeUpdate + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **title** | **kotlin.String** | | [optional] | +| **ingredients** | [**kotlin.collections.List<RecipeIngredient>**](RecipeIngredient.md) | | [optional] | +| **instructions** | **kotlin.collections.List<kotlin.String>** | | [optional] | +| **portions** | **kotlin.Double** | | [optional] | +| **nutrients** | [**RecipeNutrients**](RecipeNutrients.md) | | [optional] | + + + diff --git a/services/spring-api/docs/UserCredentials.md b/services/spring-api/docs/UserCredentials.md new file mode 100644 index 0000000..690105e --- /dev/null +++ b/services/spring-api/docs/UserCredentials.md @@ -0,0 +1,11 @@ + +# UserCredentials + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **username** | **kotlin.String** | Alphanumeric, underscores, hyphens, and dots only | [optional] | +| **password** | **kotlin.String** | | [optional] | + + + diff --git a/services/spring-api/docs/UserPreferences.md b/services/spring-api/docs/UserPreferences.md new file mode 100644 index 0000000..016a44e --- /dev/null +++ b/services/spring-api/docs/UserPreferences.md @@ -0,0 +1,20 @@ + +# UserPreferences + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **language** | [**inline**](#Language) | Preferred UI and AI-content language as an ISO 639-1 code | [optional] | +| **diet** | **kotlin.collections.List<kotlin.String>** | Dietary restriction or style (e.g. vegan, keto) | [optional] | +| **allergies** | **kotlin.collections.List<kotlin.String>** | List of ingredients the user is allergic to | [optional] | +| **aboutMe** | **kotlin.collections.List<kotlin.String>** | Free-form user context provided to the AI | [optional] | + + + +## Enum: language +| Name | Value | +| ---- | ----- | +| language | EN, DE, HU | + + + diff --git a/services/spring-api/docs/UserProfile.md b/services/spring-api/docs/UserProfile.md new file mode 100644 index 0000000..d30d3a1 --- /dev/null +++ b/services/spring-api/docs/UserProfile.md @@ -0,0 +1,11 @@ + +# UserProfile + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **username** | **kotlin.String** | | | +| **preferences** | [**UserPreferences**](UserPreferences.md) | | | + + + diff --git a/services/spring-api/docs/UserProfileUpdate.md b/services/spring-api/docs/UserProfileUpdate.md new file mode 100644 index 0000000..13f94f5 --- /dev/null +++ b/services/spring-api/docs/UserProfileUpdate.md @@ -0,0 +1,12 @@ + +# UserProfileUpdate + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **username** | **kotlin.String** | Alphanumeric, underscores, hyphens, and dots only | [optional] | +| **password** | **kotlin.String** | | [optional] | +| **preferences** | [**UserPreferences**](UserPreferences.md) | | [optional] | + + + diff --git a/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt b/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt index 68b7a43..3da9898 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt @@ -2,6 +2,8 @@ package org.openapitools.api import jakarta.validation.Valid import org.openapitools.entity.UserEntity +import org.openapitools.internal.client.HelpServiceApi +import org.openapitools.internal.client.RecipeServiceApi import org.openapitools.model.HelpRequest import org.openapitools.model.HelpResponse import org.openapitools.model.RecipeInput @@ -9,72 +11,103 @@ import org.openapitools.model.RecipeRequest import org.openapitools.model.UserPreferences import org.openapitools.model.UserProfile import org.openapitools.repository.UserRepository -import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.MediaType +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.context.SecurityContextHolder import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.server.ResponseStatusException +import retrofit2.Response import tools.jackson.databind.ObjectMapper -import java.time.Duration -import java.util.concurrent.TimeoutException +import java.io.InterruptedIOException @RestController @Validated @RequestMapping("\${api.base-path:/api/v1}") class AIApiController( - private val aiHelpWebClient: WebClient, - private val aiRecipeWebClient: WebClient, + private val helpServiceApi: HelpServiceApi, + private val recipeServiceApi: RecipeServiceApi, private val userRepository: UserRepository, private val objectMapper: ObjectMapper, ) : AIApi { - // Cap how long we wait on the GenAI service before returning an error - private val aiTimeout = Duration.ofSeconds(60) - override fun aiHelpPost( @Valid helpRequest: HelpRequest, ): ResponseEntity { val user = userRepository.findByUsername(currentUsername()).orElseThrow() - val response = - aiHelpWebClient - .post() - .uri("/ai/help") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(mapOf("profile" to user.toProfile(), "recipe" to helpRequest.recipe, "prompt" to helpRequest.prompt)) - .retrieve() - .bodyToMono(HelpResponse::class.java) - .timeout(aiTimeout) - .onErrorMap(TimeoutException::class.java) { GatewayTimeoutException("GenAI service timed out") } - .block() ?: throw BadGatewayException("GenAI service unavailable or returned an unparseable response") - return ResponseEntity.ok(response) + + val internalRequest = + org.openapitools.internal.model.HelpRequestForwarded( + profile = user.toInternalProfile(), + prompt = helpRequest.prompt, + recipe = helpRequest.recipe?.toInternalRecipe(), + ) + + val retrofitResponse = + try { + helpServiceApi.aiHelpPost("", "", internalRequest).execute() + } catch (e: InterruptedIOException) { + // Map the timeout to 504 Gateway Timeout + throw ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, "Upstream AI service timed out", e) + } catch (e: Exception) { + // Handle other potential network failures + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Upstream service error", e) + } + + val body = handleRetrofitResponse(retrofitResponse) + return ResponseEntity.ok(HelpResponse(response = body.response)) } override fun aiRecipesPost( @Valid recipeRequest: RecipeRequest, ): ResponseEntity> { val user = userRepository.findByUsername(currentUsername()).orElseThrow() - val recipes = - aiRecipeWebClient - .post() - .uri("/ai/recipes") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(mapOf("profile" to user.toProfile(), "prompt" to recipeRequest.prompt)) - .retrieve() - .bodyToMono(object : ParameterizedTypeReference>() {}) - .timeout(aiTimeout) - .onErrorMap(TimeoutException::class.java) { GatewayTimeoutException("GenAI service timed out") } - .block() ?: throw BadGatewayException("GenAI service unavailable or returned an unparseable response") - return ResponseEntity.ok(recipes) + + val internalRequest = + org.openapitools.internal.model.RecipeRequestForwarded( + profile = user.toInternalProfile(), + prompt = recipeRequest.prompt, + ) + + val retrofitResponse = + try { + recipeServiceApi.aiRecipesPost("", "", internalRequest).execute() + } catch (e: InterruptedIOException) { + throw ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, "Upstream AI service timed out", e) + } catch (e: Exception) { + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Upstream service error", e) + } + + val internalRecipes = handleRetrofitResponse(retrofitResponse) + val publicRecipes = internalRecipes.map { it.toPublicRecipe() } + return ResponseEntity.ok(publicRecipes) } private fun currentUsername(): String = SecurityContextHolder.getContext().authentication!!.name - // Converts the DB user entity into the API UserProfile model, injecting real profile data. - // Password is intentionally excluded — never forwarded to the AI service. - private fun UserEntity.toProfile(): UserProfile { - val prefs = + /** + * Helper to unwrap Retrofit responses and throw standard Spring Exceptions on failures + */ + private fun handleRetrofitResponse(response: retrofit2.Response): T { + if (!response.isSuccessful) { + throw ResponseStatusException( + HttpStatus.BAD_GATEWAY, + "Upstream service returned error: ${response.code()}", + ) + } + + return response.body() ?: throw ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Empty body", + ) + } + + // ------------------------------------------------------------------------- + // MODEL MAPPING EXTENSIONS (Translates between Public and Internal DTOs) + // ------------------------------------------------------------------------- + + private fun UserEntity.toInternalProfile(): org.openapitools.internal.model.UserProfile { + val publicPrefs = preferences?.let { try { objectMapper.readValue(it, UserPreferences::class.java) @@ -82,6 +115,61 @@ class AIApiController( null } } ?: UserPreferences() - return UserProfile(username = username, preferences = prefs) + + return org.openapitools.internal.model.UserProfile( + username = username, + preferences = + org.openapitools.internal.model.UserPreferences( + diet = publicPrefs.diet, + allergies = publicPrefs.allergies, + aboutMe = publicPrefs.aboutMe, + // safely convert between the two different generated enum types using string matching + language = + publicPrefs.language?.name?.let { enumName -> + try { + org.openapitools.internal.model.UserPreferences.Language + .valueOf(enumName) + } catch (_: IllegalArgumentException) { + null // Fallback gracefully if there's an unexpected mismatch + } + }, + ), + ) } + + private fun org.openapitools.model.RecipeInput.toInternalRecipe(): org.openapitools.internal.model.RecipeInput = + org.openapitools.internal.model.RecipeInput( + title = this.title, + portions = this.portions, + instructions = this.instructions, + ingredients = + this.ingredients.map { + org.openapitools.internal.model + .RecipeIngredient(name = it.name, quantity = it.quantity, unit = it.unit) + }, + nutrients = + this.nutrients?.let { + org.openapitools.internal.model.RecipeNutrients( + calories = it.calories, + protein = it.protein, + fat = it.fat, + carbs = it.carbs, + ) + }, + ) + + private fun org.openapitools.internal.model.RecipeInput.toPublicRecipe(): org.openapitools.model.RecipeInput = + org.openapitools.model.RecipeInput( + title = this.title, + portions = this.portions, + instructions = this.instructions, + ingredients = + this.ingredients.map { + org.openapitools.model.RecipeIngredient(name = it.name, quantity = it.quantity, unit = it.unit) + }, + nutrients = + this.nutrients?.let { + org.openapitools.model.RecipeNutrients(calories = it.calories, protein = it.protein, fat = it.fat, carbs = it.carbs) + }, + ) } diff --git a/services/spring-api/src/main/kotlin/org/openapitools/api/RecipesApiController.kt b/services/spring-api/src/main/kotlin/org/openapitools/api/RecipesApiController.kt index 421a79d..7687eed 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/api/RecipesApiController.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/api/RecipesApiController.kt @@ -39,7 +39,7 @@ class RecipesApiController( title = recipeInput.title, ingredients = objectMapper.writeValueAsString(recipeInput.ingredients), instructions = objectMapper.writeValueAsString(recipeInput.instructions), - portions = recipeInput.portions, + portions = java.math.BigDecimal.valueOf(recipeInput.portions), nutrientKcal = recipeInput.nutrients?.calories ?: 0, nutrientCarb = recipeInput.nutrients?.carbs ?: 0, nutrientProt = recipeInput.nutrients?.protein ?: 0, @@ -67,7 +67,7 @@ class RecipesApiController( recipeUpdate.title?.let { entity.title = it } recipeUpdate.ingredients?.let { entity.ingredients = objectMapper.writeValueAsString(it) } recipeUpdate.instructions?.let { entity.instructions = objectMapper.writeValueAsString(it) } - recipeUpdate.portions?.let { entity.portions = it } + recipeUpdate.portions?.let { entity.portions = java.math.BigDecimal.valueOf(it) } recipeUpdate.nutrients?.let { entity.nutrientKcal = it.calories entity.nutrientCarb = it.carbs @@ -93,7 +93,7 @@ class RecipesApiController( title = title, ingredients = objectMapper.readValue(ingredients, object : TypeReference>() {}), instructions = objectMapper.readValue(instructions, object : TypeReference>() {}), - portions = portions, + portions = portions.toDouble(), nutrients = RecipeNutrients( calories = nutrientKcal, diff --git a/services/spring-api/src/main/kotlin/org/openapitools/config/AiConfig.kt b/services/spring-api/src/main/kotlin/org/openapitools/config/AiConfig.kt index 51666f5..fc8037c 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/config/AiConfig.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/config/AiConfig.kt @@ -1,17 +1,18 @@ package org.openapitools.config +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.ConnectionPool +import okhttp3.OkHttpClient +import org.openapitools.internal.client.HelpServiceApi +import org.openapitools.internal.client.RecipeServiceApi +import org.openapitools.security.InternalHmacInterceptor import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.http.client.reactive.ReactorClientHttpConnector -import org.springframework.web.reactive.function.client.ExchangeFilterFunction -import org.springframework.web.reactive.function.client.WebClient -import reactor.netty.http.client.HttpClient -import reactor.netty.resources.ConnectionProvider -import java.nio.charset.StandardCharsets -import java.time.Duration -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit @Configuration class AiConfig { @@ -24,122 +25,42 @@ class AiConfig { @Value("\${ai.hmac.secret}") private lateinit var internalAuthSecret: String - @Bean - fun aiRecipeWebClient(): WebClient { - val pool = - ConnectionProvider - .builder("recipe") - .maxIdleTime(Duration.ofSeconds(2)) - .build() - return WebClient - .builder() - .baseUrl(aiRecipeServiceUrl) - .clientConnector(ReactorClientHttpConnector(HttpClient.create(pool))) - .filter(calculateAndAddHmacHeader()) + private fun createBaseHttpClient(): OkHttpClient = + OkHttpClient + .Builder() + .connectionPool(ConnectionPool(5, 2, TimeUnit.SECONDS)) + .addInterceptor(InternalHmacInterceptor(internalAuthSecret)) .build() - } - @Bean - fun aiHelpWebClient(): WebClient { - val pool = - ConnectionProvider - .builder("ai") - .maxIdleTime(Duration.ofSeconds(2)) - .build() - return WebClient - .builder() - .baseUrl(aiHelpServiceUrl) - .clientConnector(ReactorClientHttpConnector(HttpClient.create(pool))) - .filter(calculateAndAddHmacHeader()) + private val moshi = + Moshi + .Builder() + .addLast(KotlinJsonAdapterFactory()) .build() - } - - private fun calculateAndAddHmacHeader(): ExchangeFilterFunction { - return ExchangeFilterFunction { request, next -> - val timestamp = (System.currentTimeMillis() / 1000L).toString() - - // 1. Generate an empty-body baseline signature as a fallback - val fallbackData = "$timestamp." - val fallbackSignature = calculateHmacSha256(internalAuthSecret, fallbackData) - - // 2. Attach timestamp and baseline signature up front - val requestWithFallback = - org.springframework.web.reactive.function.client.ClientRequest - .from(request) - .header("X-Internal-Timestamp", timestamp) - .header("X-Internal-Signature", fallbackSignature) - .build() - val method = requestWithFallback.method() - - // 3. For GET, HEAD, or OPTIONS requests, no body will ever exist. Ship it immediately. - if (method == org.springframework.http.HttpMethod.GET || - method == org.springframework.http.HttpMethod.HEAD || - method == org.springframework.http.HttpMethod.OPTIONS - ) { - return@ExchangeFilterFunction next.exchange(requestWithFallback) - } - - // 4. For payload-bearing requests (POST/PUT), intercept the serialization stream - val authenticatedRequest = - org.springframework.web.reactive.function.client.ClientRequest - .from(requestWithFallback) - .body { outputMessage, context -> - val bodyDecorator = - object : org.springframework.http.client.reactive.ClientHttpRequestDecorator(outputMessage) { - override fun writeWith( - body: org.reactivestreams.Publisher, - ): reactor.core.publisher.Mono = - org.springframework.core.io.buffer.DataBufferUtils - .join(body) - .flatMap { dataBuffer -> - // Extract the raw bytes exactly as they will be sent over the wire - val bytes = ByteArray(dataBuffer.readableByteCount()) - dataBuffer.read(bytes) - org.springframework.core.io.buffer.DataBufferUtils - .release(dataBuffer) - - val payloadString = String(bytes, java.nio.charset.StandardCharsets.UTF_8) - val dataToSign = "$timestamp.$payloadString" - val signature = calculateHmacSha256(internalAuthSecret, dataToSign) - - // Dynamically overwrite the baseline signature header with the real payload signature - this.headers.set("X-Internal-Signature", signature) - - // Re-wrap the bytes into a new data buffer so the request chain can proceed natively - val factory = this.delegate.bufferFactory() - val newBuffer = factory.wrap(bytes) - super.writeWith( - reactor.core.publisher.Mono - .just(newBuffer), - ) - }.switchIfEmpty( - reactor.core.publisher.Mono.defer { - // Fallback if a POST/PUT body stream happens to be completely empty - super.writeWith(body) - }, - ) - } - request.body().insert(bodyDecorator, context) - }.build() + @Bean + fun aiHelpServiceApi(): HelpServiceApi { + val retrofit = + Retrofit + .Builder() + .baseUrl(aiHelpServiceUrl.let { if (it.endsWith("/")) it else "$it/" }) + .client(createBaseHttpClient()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() - next.exchange(authenticatedRequest) - } + return retrofit.create(HelpServiceApi::class.java) } - private fun calculateHmacSha256( - secret: String, - data: String, - ): String { - try { - val sha256HMAC = Mac.getInstance("HmacSHA256") - val secretKeySpec = SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256") - sha256HMAC.init(secretKeySpec) - val hashBytes = sha256HMAC.doFinal(data.toByteArray(StandardCharsets.UTF_8)) + @Bean + fun aiRecipeServiceApi(): RecipeServiceApi { + val retrofit = + Retrofit + .Builder() + .baseUrl(aiRecipeServiceUrl.let { if (it.endsWith("/")) it else "$it/" }) + .client(createBaseHttpClient()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() - return hashBytes.joinToString("") { "%02x".format(it) } - } catch (e: Exception) { - throw RuntimeException("Failed to generate internal HMAC authentication signature", e) - } + return retrofit.create(RecipeServiceApi::class.java) } } diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/client/HelpServiceApi.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/client/HelpServiceApi.kt new file mode 100644 index 0000000..3c0f0f3 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/client/HelpServiceApi.kt @@ -0,0 +1,47 @@ +package org.openapitools.internal.client + +import com.squareup.moshi.Json +import okhttp3.RequestBody +import org.openapitools.internal.model.ErrorResponse +import org.openapitools.internal.model.HealthGet200Response +import org.openapitools.internal.model.HelpRequestForwarded +import org.openapitools.internal.model.HelpResponse +import retrofit2.Call +import retrofit2.http.* + +interface HelpServiceApi { + /** + * POST ai/help + * Process contextual help request via LLM + * Receives a cooking query bundled with rich user profile constraints and active recipe schemas to build safe prompts. + * Responses: + * - 200: AI help generation completed successfully + * - 400: Invalid request payload syntax or corrupted internal header data (e.g. non-numeric timestamp) + * - 401: Unauthorized due to missing headers or an expired timestamp window + * - 403: Forbidden due to an invalid cryptographic HMAC validation mismatch + * - 504: Upstream LangChain LLM orchestration layer timed out + * + * @param xInternalTimestamp Linux Unix epoch timestamp string binding the generation window to block replay attacks. + * @param xInternalSignature Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. + * @param helpRequestForwarded + * @return [Call]<[HelpResponse]> + */ + @POST("ai/help") + fun aiHelpPost( + @Header("X-Internal-Timestamp") xInternalTimestamp: kotlin.String, + @Header("X-Internal-Signature") xInternalSignature: kotlin.String, + @Body helpRequestForwarded: HelpRequestForwarded, + ): Call + + /** + * GET health + * Service Health Check + * Verifies that the targeted internal microservice instance is responding normally. + * Responses: + * - 200: Service is healthy + * + * @return [Call]<[HealthGet200Response]> + */ + @GET("health") + fun healthGet(): Call +} diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/client/RecipeServiceApi.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/client/RecipeServiceApi.kt new file mode 100644 index 0000000..0b3f3e8 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/client/RecipeServiceApi.kt @@ -0,0 +1,47 @@ +package org.openapitools.internal.client + +import com.squareup.moshi.Json +import okhttp3.RequestBody +import org.openapitools.internal.model.ErrorResponse +import org.openapitools.internal.model.HealthGet200Response +import org.openapitools.internal.model.RecipeInput +import org.openapitools.internal.model.RecipeRequestForwarded +import retrofit2.Call +import retrofit2.http.* + +interface RecipeServiceApi { + /** + * POST ai/recipes + * Generate cooking recipes via LLM + * Forwards user prompt queries along with their profiles to create new recipe suggestions. + * Responses: + * - 200: AI recipe generation completed successfully + * - 400: Invalid prompt processing structure + * - 401: Missing security headers + * - 403: HMAC signature mismatch + * - 504: Upstream LangChain LLM orchestration layer timed out + * + * @param xInternalTimestamp Linux Unix epoch timestamp string binding the generation window to block replay attacks. + * @param xInternalSignature Hex-encoded HMAC-SHA256 signature validating data package integrity over private networks. + * @param recipeRequestForwarded + * @return [Call]<[kotlin.collections.List]> + */ + @POST("ai/recipes") + fun aiRecipesPost( + @Header("X-Internal-Timestamp") xInternalTimestamp: kotlin.String, + @Header("X-Internal-Signature") xInternalSignature: kotlin.String, + @Body recipeRequestForwarded: RecipeRequestForwarded, + ): Call> + + /** + * GET health + * Service Health Check + * Verifies that the targeted internal microservice instance is responding normally. + * Responses: + * - 200: Service is healthy + * + * @return [Call]<[HealthGet200Response]> + */ + @GET("health") + fun healthGet(): Call +} diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthRequest.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthRequest.kt new file mode 100644 index 0000000..97d49ff --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthRequest.kt @@ -0,0 +1,42 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param username Alphanumeric, underscores, hyphens, and dots only + * @param password + */ + +data class AuthRequest( + // Alphanumeric, underscores, hyphens, and dots only + @Json(name = "username") + val username: kotlin.String, + @Json(name = "password") + val password: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthResponse.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthResponse.kt new file mode 100644 index 0000000..4cee495 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/AuthResponse.kt @@ -0,0 +1,39 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param token JWT bearer token to include in subsequent requests + */ + +data class AuthResponse( + // JWT bearer token to include in subsequent requests + @Json(name = "token") + val token: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/ErrorResponse.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/ErrorResponse.kt new file mode 100644 index 0000000..23fa556 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/ErrorResponse.kt @@ -0,0 +1,38 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param message + */ + +data class ErrorResponse( + @Json(name = "message") + val message: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HealthGet200Response.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HealthGet200Response.kt new file mode 100644 index 0000000..41e306b --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HealthGet200Response.kt @@ -0,0 +1,38 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param status + */ + +data class HealthGet200Response( + @Json(name = "status") + val status: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequest.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequest.kt new file mode 100644 index 0000000..08ae3e6 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequest.kt @@ -0,0 +1,42 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.RecipeInput + +/** + * + * + * @param recipe + * @param prompt + */ + +data class HelpRequest( + @Json(name = "recipe") + val recipe: RecipeInput, + @Json(name = "prompt") + val prompt: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequestForwarded.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequestForwarded.kt new file mode 100644 index 0000000..a74949b --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpRequestForwarded.kt @@ -0,0 +1,46 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.RecipeInput +import org.openapitools.internal.model.UserProfile + +/** + * + * + * @param profile + * @param prompt + * @param recipe + */ + +data class HelpRequestForwarded( + @Json(name = "profile") + val profile: UserProfile, + @Json(name = "prompt") + val prompt: kotlin.String, + @Json(name = "recipe") + val recipe: RecipeInput? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpResponse.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpResponse.kt new file mode 100644 index 0000000..afa1337 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/HelpResponse.kt @@ -0,0 +1,38 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param response + */ + +data class HelpResponse( + @Json(name = "response") + val response: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/Recipe.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/Recipe.kt new file mode 100644 index 0000000..c616856 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/Recipe.kt @@ -0,0 +1,55 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.RecipeIngredient +import org.openapitools.internal.model.RecipeNutrients + +/** + * + * + * @param title + * @param ingredients + * @param instructions + * @param portions + * @param id + * @param nutrients + */ + +data class Recipe( + @Json(name = "title") + val title: kotlin.String, + @Json(name = "ingredients") + val ingredients: kotlin.collections.List, + @Json(name = "instructions") + val instructions: kotlin.collections.List, + @Json(name = "portions") + val portions: kotlin.Double, + @Json(name = "id") + val id: kotlin.Long, + @Json(name = "nutrients") + val nutrients: RecipeNutrients? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeCreated.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeCreated.kt new file mode 100644 index 0000000..4f64659 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeCreated.kt @@ -0,0 +1,38 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param id + */ + +data class RecipeCreated( + @Json(name = "id") + val id: kotlin.Long, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeIngredient.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeIngredient.kt new file mode 100644 index 0000000..3c2ee11 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeIngredient.kt @@ -0,0 +1,45 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param quantity + * @param unit Unit of measurement (e.g. g, ml, cup, tbsp) + * @param name + */ + +data class RecipeIngredient( + @Json(name = "quantity") + val quantity: kotlin.Double, + // Unit of measurement (e.g. g, ml, cup, tbsp) + @Json(name = "unit") + val unit: kotlin.String, + @Json(name = "name") + val name: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeInput.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeInput.kt new file mode 100644 index 0000000..0c50053 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeInput.kt @@ -0,0 +1,52 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.RecipeIngredient +import org.openapitools.internal.model.RecipeNutrients + +/** + * + * + * @param title + * @param ingredients + * @param instructions + * @param portions + * @param nutrients + */ + +data class RecipeInput( + @Json(name = "title") + val title: kotlin.String, + @Json(name = "ingredients") + val ingredients: kotlin.collections.List, + @Json(name = "instructions") + val instructions: kotlin.collections.List, + @Json(name = "portions") + val portions: kotlin.Double, + @Json(name = "nutrients") + val nutrients: RecipeNutrients? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeNutrients.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeNutrients.kt new file mode 100644 index 0000000..3eecdcf --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeNutrients.kt @@ -0,0 +1,51 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param calories Number of Calories (kcal) + * @param protein Protein in grams + * @param fat Fat in grams + * @param carbs Carbohydrates in grams + */ + +data class RecipeNutrients( + // Number of Calories (kcal) + @Json(name = "calories") + val calories: kotlin.Int, + // Protein in grams + @Json(name = "protein") + val protein: kotlin.Int, + // Fat in grams + @Json(name = "fat") + val fat: kotlin.Int, + // Carbohydrates in grams + @Json(name = "carbs") + val carbs: kotlin.Int, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequest.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequest.kt new file mode 100644 index 0000000..5c56c99 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequest.kt @@ -0,0 +1,38 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param prompt + */ + +data class RecipeRequest( + @Json(name = "prompt") + val prompt: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequestForwarded.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequestForwarded.kt new file mode 100644 index 0000000..68e2476 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeRequestForwarded.kt @@ -0,0 +1,42 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.UserProfile + +/** + * + * + * @param profile + * @param prompt + */ + +data class RecipeRequestForwarded( + @Json(name = "profile") + val profile: UserProfile, + @Json(name = "prompt") + val prompt: kotlin.String, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeUpdate.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeUpdate.kt new file mode 100644 index 0000000..ccb0025 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/RecipeUpdate.kt @@ -0,0 +1,52 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.RecipeIngredient +import org.openapitools.internal.model.RecipeNutrients + +/** + * At least one field must be provided + * + * @param title + * @param ingredients + * @param instructions + * @param portions + * @param nutrients + */ + +data class RecipeUpdate( + @Json(name = "title") + val title: kotlin.String? = null, + @Json(name = "ingredients") + val ingredients: kotlin.collections.List? = null, + @Json(name = "instructions") + val instructions: kotlin.collections.List? = null, + @Json(name = "portions") + val portions: kotlin.Double? = null, + @Json(name = "nutrients") + val nutrients: RecipeNutrients? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserCredentials.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserCredentials.kt new file mode 100644 index 0000000..26f9089 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserCredentials.kt @@ -0,0 +1,42 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Reusable field definitions for username and password constraints + * + * @param username Alphanumeric, underscores, hyphens, and dots only + * @param password + */ + +data class UserCredentials( + // Alphanumeric, underscores, hyphens, and dots only + @Json(name = "username") + val username: kotlin.String? = null, + @Json(name = "password") + val password: kotlin.String? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserPreferences.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserPreferences.kt new file mode 100644 index 0000000..32181dd --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserPreferences.kt @@ -0,0 +1,70 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param language Preferred UI and AI-content language as an ISO 639-1 code + * @param diet Dietary restriction or style (e.g. vegan, keto) + * @param allergies List of ingredients the user is allergic to + * @param aboutMe Free-form user context provided to the AI + */ + +data class UserPreferences( + // Preferred UI and AI-content language as an ISO 639-1 code + @Json(name = "language") + val language: UserPreferences.Language? = null, + // Dietary restriction or style (e.g. vegan, keto) + @Json(name = "diet") + val diet: kotlin.collections.List? = null, + // List of ingredients the user is allergic to + @Json(name = "allergies") + val allergies: kotlin.collections.List? = null, + // Free-form user context provided to the AI + @Json(name = "aboutMe") + val aboutMe: kotlin.collections.List? = null, +) { + /** + * Preferred UI and AI-content language as an ISO 639-1 code + * + * Values: EN,DE,HU + */ + @JsonClass(generateAdapter = false) + enum class Language( + val value: kotlin.String, + ) { + @Json(name = "EN") + EN("EN"), + + @Json(name = "DE") + DE("DE"), + + @Json(name = "HU") + HU("HU"), + } +} diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfile.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfile.kt new file mode 100644 index 0000000..4dc0b68 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfile.kt @@ -0,0 +1,42 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.UserPreferences + +/** + * + * + * @param username + * @param preferences + */ + +data class UserProfile( + @Json(name = "username") + val username: kotlin.String, + @Json(name = "preferences") + val preferences: UserPreferences, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfileUpdate.kt b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfileUpdate.kt new file mode 100644 index 0000000..4f4b0be --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/internal/model/UserProfileUpdate.kt @@ -0,0 +1,46 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "DuplicatedCode", + "EnumEntryName", + "RemoveRedundantQualifierName", + "RemoveRedundantCallsOfConversionMethods", + "REDUNDANT_CALL_OF_CONVERSION_METHOD", + "RedundantUnitReturnType", + "RemoveEmptyClassBody", + "UnnecessaryVariable", + "UnusedImport", + "UnnecessaryVariable", + "unused", +) + +package org.openapitools.internal.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.openapitools.internal.model.UserPreferences + +/** + * At least one field must be provided + * + * @param username Alphanumeric, underscores, hyphens, and dots only + * @param password + * @param preferences + */ + +data class UserProfileUpdate( + // Alphanumeric, underscores, hyphens, and dots only + @Json(name = "username") + val username: kotlin.String? = null, + @Json(name = "password") + val password: kotlin.String? = null, + @Json(name = "preferences") + val preferences: UserPreferences? = null, +) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/model/Recipe.kt b/services/spring-api/src/main/kotlin/org/openapitools/model/Recipe.kt index e65f77d..4040115 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/model/Recipe.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/model/Recipe.kt @@ -37,7 +37,7 @@ data class Recipe( @get:JsonProperty("instructions", required = true) val instructions: kotlin.collections.List, @get:DecimalMin(value = "0.5") @Schema(example = "null", required = true, description = "") - @get:JsonProperty("portions", required = true) val portions: java.math.BigDecimal, + @get:JsonProperty("portions", required = true) val portions: kotlin.Double, @get:Min(value = 1L) @Schema(example = "null", required = true, description = "") @get:JsonProperty("id", required = true) val id: kotlin.Long, diff --git a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeIngredient.kt b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeIngredient.kt index 7ddff67..79a841b 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeIngredient.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeIngredient.kt @@ -22,7 +22,7 @@ import java.util.Objects data class RecipeIngredient( @get:DecimalMin(value = "0") @Schema(example = "null", required = true, description = "") - @get:JsonProperty("quantity", required = true) val quantity: java.math.BigDecimal, + @get:JsonProperty("quantity", required = true) val quantity: kotlin.Double, @get:Size(min = 1) @Schema(example = "null", required = true, description = "Unit of measurement (e.g. g, ml, cup, tbsp)") @get:JsonProperty("unit", required = true) val unit: kotlin.String, diff --git a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeInput.kt b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeInput.kt index 651b49e..7086749 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeInput.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeInput.kt @@ -36,7 +36,7 @@ data class RecipeInput( @get:JsonProperty("instructions", required = true) val instructions: kotlin.collections.List, @get:DecimalMin(value = "0.5") @Schema(example = "null", required = true, description = "") - @get:JsonProperty("portions", required = true) val portions: java.math.BigDecimal, + @get:JsonProperty("portions", required = true) val portions: kotlin.Double, @field:Valid @Schema(example = "null", description = "") @get:JsonProperty("nutrients") val nutrients: RecipeNutrients? = null, diff --git a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeUpdate.kt b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeUpdate.kt index 125985f..565f3f9 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeUpdate.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeUpdate.kt @@ -36,7 +36,7 @@ data class RecipeUpdate( @get:JsonProperty("instructions") val instructions: kotlin.collections.List? = null, @get:DecimalMin(value = "0.5") @Schema(example = "null", description = "") - @get:JsonProperty("portions") val portions: java.math.BigDecimal? = null, + @get:JsonProperty("portions") val portions: kotlin.Double? = null, @field:Valid @Schema(example = "null", description = "") @get:JsonProperty("nutrients") val nutrients: RecipeNutrients? = null, diff --git a/services/spring-api/src/main/kotlin/org/openapitools/security/InternalHmacInterceptor.kt b/services/spring-api/src/main/kotlin/org/openapitools/security/InternalHmacInterceptor.kt new file mode 100644 index 0000000..fb77939 --- /dev/null +++ b/services/spring-api/src/main/kotlin/org/openapitools/security/InternalHmacInterceptor.kt @@ -0,0 +1,50 @@ +package org.openapitools.security + +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class InternalHmacInterceptor( + private val hmacSecret: String, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + + val timestamp = (System.currentTimeMillis() / 1000).toString() + + val bodyBytes = + if (original.body != null) { + val buffer = Buffer() + original.body!!.writeTo(buffer) + buffer.readByteArray() + } else { + ByteArray(0) + } + + val hexSignature = + try { + val mac = Mac.getInstance("HmacSHA256") + val secretKey = SecretKeySpec(hmacSecret.toByteArray(Charsets.UTF_8), "HmacSHA256") + mac.init(secretKey) + + mac.update(timestamp.toByteArray(Charsets.UTF_8)) + mac.update('.'.code.toByte()) + val rawHmac = mac.doFinal(bodyBytes) + + rawHmac.joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + "" + } + + val requestBuilder = + original + .newBuilder() + .header("X-Internal-Timestamp", timestamp) + .header("X-Internal-Signature", hexSignature) + + return chain.proceed(requestBuilder.build()) + } +} diff --git a/services/spring-api/src/main/resources/application.yaml b/services/spring-api/src/main/resources/application.yaml index 268399e..5c2b560 100644 --- a/services/spring-api/src/main/resources/application.yaml +++ b/services/spring-api/src/main/resources/application.yaml @@ -5,7 +5,7 @@ spring: default-property-inclusion: non_null datasource: # Locally defaults to H2 in-memory. On GCP Cloud Run, set DB_URL (e.g. jdbc:postgresql:///?socketFactory=...&cloudSqlInstance=...), DB_USER, DB_PASSWORD env vars - url: ${DB_URL:jdbc:h2:mem:cookingdb;DB_CLOSE_DELAY=-1} + url: ${DB_URL:jdbc:h2:mem:cookingdb;DB_CLOSE_DELAY=-1;INIT=ANALYZE} username: ${DB_USER:sa} password: ${DB_PASSWORD:} jpa: @@ -13,6 +13,10 @@ spring: # create-drop: wipe and recreate tables every restart # update: keep data between restarts ddl-auto: ${JPA_DDL_AUTO:create-drop} + properties: + hibernate: + connection: + provider_disables_autocommit: false show-sql: false open-in-view: false mvc: diff --git a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt index 57d360b..c7b8c18 100644 --- a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt +++ b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt @@ -1,5 +1,7 @@ package org.openapitools.api +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -8,7 +10,8 @@ import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.whenever -import org.openapitools.model.HelpResponse +import org.openapitools.internal.client.HelpServiceApi +import org.openapitools.internal.client.RecipeServiceApi import org.openapitools.model.RecipeIngredient import org.openapitools.model.RecipeInput import org.openapitools.model.RecipeNutrients @@ -16,68 +19,102 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import -import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Mono +import org.springframework.web.server.ResponseStatusException +import retrofit2.Call +import retrofit2.Response +import java.io.InterruptedIOException import java.math.BigDecimal -@Import(AIApiTest.MockWebClients::class) +@Import(AIApiTest.MockApiServices::class) class AIApiTest : ApiTestBase() { - // Provides deep-stub WebClient mocks so the full fluent chain can be stubbed @TestConfiguration - class MockWebClients { - val helpClient: WebClient = Mockito.mock(WebClient::class.java, Mockito.RETURNS_DEEP_STUBS) - val recipeClient: WebClient = Mockito.mock(WebClient::class.java, Mockito.RETURNS_DEEP_STUBS) + class MockApiServices { + val helpServiceApi: HelpServiceApi = Mockito.mock(HelpServiceApi::class.java) + val recipeServiceApi: RecipeServiceApi = Mockito.mock(RecipeServiceApi::class.java) - @Bean fun aiHelpWebClient(): WebClient = helpClient + @Bean fun helpServiceApi(): HelpServiceApi = helpServiceApi - @Bean fun aiRecipeWebClient(): WebClient = recipeClient + @Bean fun recipeServiceApi(): RecipeServiceApi = recipeServiceApi } - @Autowired lateinit var mockWebClients: MockWebClients + @Autowired lateinit var mockApiServices: MockApiServices @BeforeEach fun resetMocks() { - Mockito.reset(mockWebClients.helpClient, mockWebClients.recipeClient) - } - - private fun stubHelpClient(response: Mono) { - @Suppress("UNCHECKED_CAST") - whenever( - mockWebClients.helpClient - .post() - .uri("/ai/help") - .contentType(any()) - .bodyValue(any()) - .retrieve() - .bodyToMono(HelpResponse::class.java), - ).thenReturn(response) - } - - private fun stubRecipeClient(response: Mono>) { - @Suppress("UNCHECKED_CAST") - whenever( - mockWebClients.recipeClient - .post() - .uri("/ai/recipes") - .contentType(any()) - .bodyValue(any()) - .retrieve() - .bodyToMono(any>>()), - ).thenReturn(response) - } - - private val sampleRecipeInput = - RecipeInput( + Mockito.reset(mockApiServices.helpServiceApi, mockApiServices.recipeServiceApi) + } + + // Helper to generate a mock Retrofit Call object wrapping a successful response + private fun createMockCall(body: T?): Call { + val mockCall = Mockito.mock(Call::class.java) as Call + val response = Response.success(body) + whenever(mockCall.execute()).thenReturn(response) + return mockCall + } + + // Helper to generate a mock Retrofit Call object wrapping an HTTP error code response + private fun createMockErrorCall(statusCode: Int): Call { + val mockCall = Mockito.mock(Call::class.java) as Call + val response = Response.error(statusCode, "Internal Error".toResponseBody("application/json".toMediaType())) + whenever(mockCall.execute()).thenReturn(response) + return mockCall + } + + // Helper to generate a mock Retrofit Call that throws a low-level network timeout exception + private fun createMockTimeoutCall(): Call { + val mockCall = Mockito.mock(Call::class.java) as Call + whenever(mockCall.execute()).thenThrow(InterruptedIOException("timeout")) + return mockCall + } + + private fun stubHelpClient(body: org.openapitools.internal.model.HelpResponse?) { + val callStub = createMockCall(body) + whenever(mockApiServices.helpServiceApi.aiHelpPost(any(), any(), any())).thenReturn(callStub) + } + + private fun stubHelpClientError(statusCode: Int) { + val callStub = createMockErrorCall(statusCode) + whenever(mockApiServices.helpServiceApi.aiHelpPost(any(), any(), any())).thenReturn(callStub) + } + + private fun stubHelpClientTimeout() { + val callStub = createMockTimeoutCall() + whenever(mockApiServices.helpServiceApi.aiHelpPost(any(), any(), any())).thenReturn(callStub) + } + + private fun stubRecipeClient(body: List?) { + val callStub = createMockCall(body) + whenever(mockApiServices.recipeServiceApi.aiRecipesPost(any(), any(), any())).thenReturn(callStub) + } + + private fun stubRecipeClientError(statusCode: Int) { + val callStub = createMockErrorCall>(statusCode) + whenever(mockApiServices.recipeServiceApi.aiRecipesPost(any(), any(), any())).thenReturn(callStub) + } + + private fun stubRecipeClientTimeout() { + val callStub = createMockTimeoutCall>() + whenever(mockApiServices.recipeServiceApi.aiRecipesPost(any(), any(), any())).thenReturn(callStub) + } + + private val sampleInternalRecipeInput = + org.openapitools.internal.model.RecipeInput( title = "AI Pasta", - ingredients = listOf(RecipeIngredient(quantity = BigDecimal("200"), unit = "g", name = "pasta")), + ingredients = + listOf( + org.openapitools.internal.model + .RecipeIngredient(quantity = 200.0, unit = "g", name = "pasta"), + ), instructions = listOf("Boil water", "Cook pasta"), - portions = BigDecimal("2"), - nutrients = RecipeNutrients(calories = 400, protein = 12, fat = 2, carbs = 70), + portions = 2.0, + nutrients = + org.openapitools.internal.model + .RecipeNutrients(calories = 400, protein = 12, fat = 2, carbs = 70), ) // --- POST /ai/help --- @@ -85,7 +122,11 @@ class AIApiTest : ApiTestBase() { @Test fun `ai help - returns 200 with AI response`() { val token = register() - stubHelpClient(Mono.just(HelpResponse("Try adding more salt."))) + stubHelpClient( + org.openapitools.internal.model + .HelpResponse("Try adding more salt."), + ) + mockMvc .perform( post("/api/v1/ai/help") @@ -99,9 +140,10 @@ class AIApiTest : ApiTestBase() { } @Test - fun `ai help - service returns null returns 502`() { + fun `ai help - service returns error returns 502`() { val token = register() - stubHelpClient(Mono.empty()) + stubHelpClientError(500) // Simulates a 500 failure from Python + mockMvc .perform( post("/api/v1/ai/help") @@ -111,7 +153,22 @@ class AIApiTest : ApiTestBase() { """{"recipe":{"title":"Pasta","ingredients":[{"quantity":200,"unit":"g","name":"pasta"}],"instructions":["Cook"],"portions":2},"prompt":"Help?"}""", ), ).andExpect(status().isBadGateway) - .andExpect(jsonPath("$.message").exists()) + } + + @Test + fun `ai help - connection timeouts return 504`() { + val token = register() + stubHelpClientTimeout() // Simulates the 60 second OkHttp trigger limit aborting + + mockMvc + .perform( + post("/api/v1/ai/help") + .header("Authorization", "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .content( + """{"recipe":{"title":"Pasta","ingredients":[{"quantity":200,"unit":"g","name":"pasta"}],"instructions":["Cook"],"portions":2},"prompt":"Help?"}""", + ), + ).andExpect(status().isGatewayTimeout) } @Test @@ -124,7 +181,6 @@ class AIApiTest : ApiTestBase() { """{"recipe":{"title":"x","ingredients":[{"quantity":1,"unit":"g","name":"x"}],"instructions":["x"],"portions":1},"prompt":"x"}""", ), ).andExpect(status().isUnauthorized) - .andExpect(jsonPath("$.message").exists()) } @Test @@ -136,7 +192,6 @@ class AIApiTest : ApiTestBase() { .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON), ).andExpect(status().isBadRequest) - .andExpect(jsonPath("$.message").exists()) } @ParameterizedTest(name = "field: {0}") @@ -172,7 +227,8 @@ class AIApiTest : ApiTestBase() { @Test fun `ai recipes - returns 200 with generated recipes`() { val token = register() - stubRecipeClient(Mono.just(listOf(sampleRecipeInput))) + stubRecipeClient(listOf(sampleInternalRecipeInput)) + mockMvc .perform( post("/api/v1/ai/recipes") @@ -185,9 +241,10 @@ class AIApiTest : ApiTestBase() { } @Test - fun `ai recipes - service returns null returns 502`() { + fun `ai recipes - service returns error returns 502`() { val token = register() - stubRecipeClient(Mono.empty()) + stubRecipeClientError(500) + mockMvc .perform( post("/api/v1/ai/recipes") @@ -195,7 +252,20 @@ class AIApiTest : ApiTestBase() { .contentType(MediaType.APPLICATION_JSON) .content("""{"prompt":"Give me a pasta recipe"}"""), ).andExpect(status().isBadGateway) - .andExpect(jsonPath("$.message").exists()) + } + + @Test + fun `ai recipes - connection timeouts return 504`() { + val token = register() + stubRecipeClientTimeout() + + mockMvc + .perform( + post("/api/v1/ai/recipes") + .header("Authorization", "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .content("""{"prompt":"Give me a pasta recipe"}"""), + ).andExpect(status().isGatewayTimeout) } @Test @@ -206,7 +276,6 @@ class AIApiTest : ApiTestBase() { .contentType(MediaType.APPLICATION_JSON) .content("""{"prompt":"Give me a recipe"}"""), ).andExpect(status().isUnauthorized) - .andExpect(jsonPath("$.message").exists()) } @Test @@ -218,6 +287,5 @@ class AIApiTest : ApiTestBase() { .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON), ).andExpect(status().isBadRequest) - .andExpect(jsonPath("$.message").value("Missing or malformed request body")) } } diff --git a/web-client/src/api.ts b/web-client/src/api.ts index c529fcd..7d4e871 100644 --- a/web-client/src/api.ts +++ b/web-client/src/api.ts @@ -785,19 +785,10 @@ export interface components { RecipeRequest: { prompt: string; }; - RecipeRequestForwarded: { - profile: components["schemas"]["UserProfile"]; - prompt: string; - }; HelpRequest: { recipe: components["schemas"]["RecipeInput"]; prompt: string; }; - HelpRequestForwarded: { - profile: components["schemas"]["UserProfile"]; - recipe: components["schemas"]["RecipeInput"]; - prompt: string; - }; HelpResponse: { response: string; };