Repository: airtai/fastagency

Issue:
Bug: unable to parse OpenAPI spec
**Describe the bug**

When trying to create `OpenAPI` object using `OpenAPI.create()`, we get the following error:
```
---------------------------------------------------------------------------
InvalidInput                              Traceback (most recent call last)
Cell In[7], [line 1](vscode-notebook-cell:?execution_count=7&line=1)
----> [1](vscode-notebook-cell:?execution_count=7&line=1) OpenAPI.create(openapi_json=get_openapi_spec())

File /work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:297, in OpenAPI.create(cls, openapi_json, openapi_url)
    [294](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:294) td = Path(temp_dir)
    [295](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:295) suffix = td.name
--> [297](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:297) main_name = cls.generate_code(
    [298](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:298)     input_text=openapi_json,  # type: ignore [arg-type]
    [299](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:299)     output_dir=td,
    [300](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:300) )
    [301](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:301) # add td to sys.path
    [302](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:302) try:

File /work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:242, in OpenAPI.generate_code(cls, input_text, output_dir, disable_timestamp, custom_visitors)
    [239](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:239) custom_visitors.append(Path(__file__).parent / "security_schema_visitor.py")
    [241](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:241) with patch_get_parameter_type():
--> [242](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:242)     generate_code(
    [243](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:243)         input_name="openapi.json",
    [244](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:244)         input_text=input_text,
    [245](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:245)         encoding="utf-8",
    [246](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:246)         output_dir=output_dir,
    [247](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:247)         template_dir=cls._get_template_dir(),
    [248](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:248)         disable_timestamp=disable_timestamp,
    [249](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:249)         custom_visitors=custom_visitors,
    [250](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:250)     )
    [251](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:251)     # Use unique file name for main.py
    [252](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastagency/api/openapi/client.py:252)     main_name = f"main_{output_dir.name}"

File /work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:201, in generate_code(input_name, input_text, encoding, output_dir, template_dir, model_path, enum_field_as_literal, custom_visitors, disable_timestamp, generate_routers, specify_tags, output_model_type, python_version)
    [199](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:199)     template = environment.get_template(str(relative_path))
    [200](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:200)     result = template.render(template_vars)
--> [201](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:201)     results[relative_path] = code_formatter.format_code(result)
    [203](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:203) if generate_routers:
    [204](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/fastapi_code_generator/__main__.py:204)     tags = sorted_tags

File /work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:226, in CodeFormatter.format_code(self, code)
    [221](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:221) def format_code(
    [222](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:222)     self,
    [223](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:223)     code: str,
    [224](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:224) ) -> str:
    [225](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:225)     code = self.apply_isort(code)
--> [226](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:226)     code = self.apply_black(code)
    [228](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:228)     for formatter in self.custom_formatters:
    [229](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:229)         code = formatter.apply(code)

File /work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:234, in CodeFormatter.apply_black(self, code)
    [233](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:233) def apply_black(self, code: str) -> str:
--> [234](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:234)     return black.format_str(
    [235](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:235)         code,
    [236](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:236)         mode=self.black_mode,
    [237](https://vscode-remote+ssh-002dremote-002bairt.vscode-resource.vscode-cdn.net/work/davor/projects/airt/context-leakage-team/.venv/lib/python3.10/site-packages/datamodel_code_generator/format.py:237)     )

File src/black/__init__.py:1225, in format_str()

File src/black/__init__.py:1239, in _format_str_once()

File src/black/parsing.py:90, in lib2to3_parse()

InvalidInput: Cannot parse: 51:4:     ,
```


[openapi.json](https://github.com/user-attachments/files/17220965/openapi.json)

**How to reproduce**

```python
OpenAPI.create(openapi_json=get_openapi_spec())
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/airtai/fastagency/pull/303?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) Report
Attention: Patch coverage is `80.00000%` with `7 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/airtai/fastagency/pull/303?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) | Patch % | Lines |
|---|---|---|
| [fastagency/api/openapi/security.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fsecurity.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9zZWN1cml0eS5weQ==) | 86.95% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) |
| [fastagency/api/openapi/client.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fclient.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9jbGllbnQucHk=) | 75.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) |
| [fastagency/api/openapi/security\_schema\_visitor.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fsecurity_schema_visitor.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9zZWN1cml0eV9zY2hlbWFfdmlzaXRvci5weQ==) | 50.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) |

| [Files with missing lines](https://app.codecov.io/gh/airtai/fastagency/pull/303?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai) | Coverage Δ | |
|---|---|---|
| [fastagency/api/openapi/client.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fclient.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9jbGllbnQucHk=) | `84.93% <75.00%> (-0.79%)` | :arrow_down: |
| [fastagency/api/openapi/security\_schema\_visitor.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fsecurity_schema_visitor.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9zZWN1cml0eV9zY2hlbWFfdmlzaXRvci5weQ==) | `80.00% <50.00%> (+2.22%)` | :arrow_up: |
| [fastagency/api/openapi/security.py](https://app.codecov.io/gh/airtai/fastagency/pull/303?src=pr&el=tree&filepath=fastagency%2Fapi%2Fopenapi%2Fsecurity.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai#diff-ZmFzdGFnZW5jeS9hcGkvb3BlbmFwaS9zZWN1cml0eS5weQ==) | `83.14% <86.95%> (+0.79%)` | :arrow_up: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/airtai/fastagency/pull/303/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airtai)


Patch:
diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md
index 57ff9f36..ba2d138c 100644
--- a/docs/docs/SUMMARY.md
+++ b/docs/docs/SUMMARY.md
@@ -50,6 +50,7 @@ search:
                         - [BaseSecurity](api/fastagency/api/openapi/security/BaseSecurity.md)
                         - [BaseSecurityParameters](api/fastagency/api/openapi/security/BaseSecurityParameters.md)
                         - [HTTPBearer](api/fastagency/api/openapi/security/HTTPBearer.md)
+                        - [OAuth2PasswordBearer](api/fastagency/api/openapi/security/OAuth2PasswordBearer.md)
                     - security_schema_visitor
                         - [custom_visitor](api/fastagency/api/openapi/security_schema_visitor/custom_visitor.md)
             - app
@@ -99,6 +100,7 @@ search:
                 - [check_imports](api/fastagency/helpers/check_imports.md)
                 - [extract_json_objects](api/fastagency/helpers/extract_json_objects.md)
                 - [jsonify_string](api/fastagency/helpers/jsonify_string.md)
+                - [optional_temp_path](api/fastagency/helpers/optional_temp_path.md)
             - logging
                 - [get_logger](api/fastagency/logging/get_logger.md)
             - runtime
diff --git a/docs/docs/en/api/fastagency/api/openapi/security/OAuth2PasswordBearer.md b/docs/docs/en/api/fastagency/api/openapi/security/OAuth2PasswordBearer.md
new file mode 100644
index 00000000..ccdf5027
--- /dev/null
+++ b/docs/docs/en/api/fastagency/api/openapi/security/OAuth2PasswordBearer.md
@@ -0,0 +1,11 @@
+---
+# 0.5 - API
+# 2 - Release
+# 3 - Contributing
+# 5 - Template Page
+# 10 - Default
+search:
+  boost: 0.5
+---
+
+::: fastagency.api.openapi.security.OAuth2PasswordBearer
diff --git a/docs/docs/en/api/fastagency/helpers/optional_temp_path.md b/docs/docs/en/api/fastagency/helpers/optional_temp_path.md
new file mode 100644
index 00000000..8d45189d
--- /dev/null
+++ b/docs/docs/en/api/fastagency/helpers/optional_temp_path.md
@@ -0,0 +1,11 @@
+---
+# 0.5 - API
+# 2 - Release
+# 3 - Contributing
+# 5 - Template Page
+# 10 - Default
+search:
+  boost: 0.5
+---
+
+::: fastagency.helpers.optional_temp_path
diff --git a/fastagency/api/openapi/client.py b/fastagency/api/openapi/client.py
index ec77d60b..419e0fca 100644
--- a/fastagency/api/openapi/client.py
+++ b/fastagency/api/openapi/client.py
@@ -3,7 +3,6 @@ import inspect
 import re
 import shutil
 import sys
-import tempfile
 from collections.abc import Iterable, Iterator, Mapping
 from contextlib import contextmanager
 from functools import wraps
@@ -16,6 +15,8 @@ import requests
 from fastapi_code_generator.__main__ import generate_code
 from pydantic_core import PydanticUndefined
 
+from fastagency.helpers import optional_temp_path
+
 from .fastapi_code_generator_helpers import patch_get_parameter_type
 from .security import BaseSecurity, BaseSecurityParameters
 
@@ -280,9 +281,12 @@ class OpenAPI:
 
     @classmethod
     def create(
-        cls, openapi_json: Optional[str] = None, openapi_url: Optional[str] = None
+        cls,
+        openapi_json: Optional[str] = None,
+        openapi_url: Optional[str] = None,
+        client_source_path: Optional[str] = None,
     ) -> "OpenAPI":
-        if openapi_json is None and openapi_url is None:
+        if (openapi_json is None) == (openapi_url is None):
             raise ValueError("Either openapi_json or openapi_url should be provided")
 
         if openapi_json is None and openapi_url is not None:
@@ -290,8 +294,7 @@ class OpenAPI:
                 response.raise_for_status()
                 openapi_json = response.text
 
-        with tempfile.TemporaryDirectory() as temp_dir:
-            td = Path(temp_dir)
+        with optional_temp_path(client_source_path) as td:
             suffix = td.name
 
             main_name = cls.generate_code(
diff --git a/fastagency/api/openapi/security.py b/fastagency/api/openapi/security.py
index 601ff3d2..5f8b40d6 100644
--- a/fastagency/api/openapi/security.py
+++ b/fastagency/api/openapi/security.py
@@ -1,5 +1,5 @@
 import logging
-from typing import Any, ClassVar, Literal, Optional, Protocol
+from typing import Any, ClassVar, Literal, Optional, Protocol, Union
 
 from pydantic import BaseModel, model_validator
 
@@ -36,7 +36,7 @@ class BaseSecurity(BaseModel):
         return isinstance(self, security_params.get_security_class())
 
     @classmethod
-    def is_supported(cls, type: str, in_value: str) -> bool:
+    def is_supported(cls, type: str, in_value: Union[str, dict[str, Any]]) -> bool:
         return type == cls.type and in_value == cls.in_value
 
     @classmethod
@@ -50,7 +50,9 @@ class BaseSecurity(BaseModel):
             logger.error(
                 f"Unsupported type '{type}' and in_value '{in_value}' combination"
             )
-            return None
+            raise ValueError(
+                f"Unsupported type '{type}' and in_value '{in_value}' combination"
+            )
 
 
 class BaseSecurityParameters(Protocol):
@@ -171,3 +173,53 @@ class HTTPBearer(BaseSecurity):
 
         def get_security_class(self) -> type[BaseSecurity]:
             return HTTPBearer
+
+
+class OAuth2PasswordBearer(BaseSecurity):
+    """OAuth2 Password Bearer security class."""
+
+    type: ClassVar[Literal["oauth2"]] = "oauth2"
+    in_value: ClassVar[Literal["bearer"]] = "bearer"
+
+    @classmethod
+    def is_supported(cls, type: str, in_value: Union[str, dict[str, Any]]) -> bool:
+        return type == cls.type and isinstance(in_value, dict)
+
+    class Parameters(BaseModel):  # BaseSecurityParameters
+        """OAuth2 Password Bearer security class."""
+
+        username: Optional[str] = None
+        password: Optional[str] = None
+        bearer_token: Optional[str] = None
+
+        @model_validator(mode="before")
+        def check_credentials(cls, values: dict[str, Any]) -> Any:  # noqa
+            username = values.get("username")
+            password = values.get("password")
+            bearer_token = values.get("bearer_token")
+
+            if not bearer_token and (not username or not password):
+                # If bearer_token is not provided, both username and password must be defined
+                raise ValueError(
+                    "Both username and password are required if bearer_token is not provided."
+                )
+
+            return values
+
+        def apply(
+            self,
+            q_params: dict[str, Any],
+            body_dict: dict[str, Any],
+            security: BaseSecurity,
+        ) -> None:
+            if not self.bearer_token:
+                # request token from the tokenUrl with username and password
+                raise NotImplementedError()
+
+            if "headers" not in body_dict:
+                body_dict["headers"] = {}
+
+            body_dict["headers"]["Authorization"] = f"Bearer {self.bearer_token}"
+
+        def get_security_class(self) -> type[BaseSecurity]:
+            return OAuth2PasswordBearer
diff --git a/fastagency/api/openapi/security_schema_visitor.py b/fastagency/api/openapi/security_schema_visitor.py
index db210e48..581b9fba 100644
--- a/fastagency/api/openapi/security_schema_visitor.py
+++ b/fastagency/api/openapi/security_schema_visitor.py
@@ -17,8 +17,10 @@ def custom_visitor(parser: OpenAPIParser, model_path: Path) -> dict[str, object]
     security_classes = []
     security_parameters = {}
     for k, v in security_schemes.items():
-        if "in" not in v:
-            in_value = v["scheme"] if "scheme" in v and v["type"] == "http" else None
+        if "in" not in v and v["type"] == "http":
+            in_value = v.get("scheme", None)
+        if "in" not in v and v["type"] == "oauth2":
+            in_value = v.get("flows", None)
         else:
             in_value = v["in"]
         security_class = BaseSecurity.get_security_class(
diff --git a/fastagency/helpers.py b/fastagency/helpers.py
index d13d90fd..92eb459e 100644
--- a/fastagency/helpers.py
+++ b/fastagency/helpers.py
@@ -1,7 +1,10 @@
 import importlib
 import json
+import tempfile
 from collections.abc import Iterator
+from contextlib import contextmanager
 from json import JSONDecoder
+from pathlib import Path
 from typing import Optional
 
 __all__ = ["check_imports"]
@@ -54,3 +57,12 @@ def jsonify_string(line: str) -> str:
     # (don't make that a list comprehension, quite un-readable)
 
     return "".join(line_parts)
+
+
+@contextmanager
+def optional_temp_path(path: Optional[str] = None) -> Iterator[Path]:
+    if path is None:
+        with tempfile.TemporaryDirectory() as temp_dir:
+            yield Path(temp_dir)
+    else:
+        yield Path(path)


Test patch:
diff --git a/tests/api/openapi/security/test_oauth_client.py b/tests/api/openapi/security/test_oauth_client.py
new file mode 100644
index 00000000..bad1c456
--- /dev/null
+++ b/tests/api/openapi/security/test_oauth_client.py
@@ -0,0 +1,162 @@
+import json
+from typing import Annotated, Any
+
+import pytest
+import requests
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer as FastAPIOAuth2PasswordBearer
+
+from fastagency.api.openapi import OpenAPI
+from fastagency.api.openapi.security import OAuth2PasswordBearer
+
+
+def create_oauth2_fastapi_app(host: str, port: int) -> FastAPI:
+    app = FastAPI(
+        title="OAuth2",
+        servers=[
+            {"url": f"http://{host}:{port}", "description": "Local development server"}
+        ],
+    )
+
+    oauth2_scheme = FastAPIOAuth2PasswordBearer(tokenUrl="token")
+
+    @app.post("/low", summary="Low Level")
+    async def post_oauth(
+        message: str, token: Annotated[str, Depends(oauth2_scheme)]
+    ) -> dict[str, str]:
+        if token != "token123":
+            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
+        return {"message": message}
+
+    return app
+
+
+@pytest.fixture
+def openapi_oauth2_schema() -> dict[str, Any]:
+    return {
+        "openapi": "3.1.0",
+        "info": {"title": "OAuth2", "version": "0.1.0"},
+        "servers": [
+            {"url": "http://127.0.0.1:43465", "description": "Local development server"}
+        ],
+        "paths": {
+            "/low": {
+                "post": {
+                    "summary": "Low Level",
+                    "operationId": "post_oauth_low_post",
+                    "security": [{"OAuth2PasswordBearer": []}],
+                    "parameters": [
+                        {
+                            "name": "message",
+                            "in": "query",
+                            "required": True,
+                            "schema": {"type": "string", "title": "Message"},
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "type": "object",
+                                        "additionalProperties": {"type": "string"},
+                                        "title": "Response Post Oauth Low Post",
+                                    }
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            },
+            "securitySchemes": {
+                "OAuth2PasswordBearer": {
+                    "type": "oauth2",
+                    "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
+                }
+            },
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "fastapi_openapi_url",
+    [(create_oauth2_fastapi_app)],
+    indirect=["fastapi_openapi_url"],
+)
+def test_oauth2_fastapi_app(
+    fastapi_openapi_url: str, openapi_oauth2_schema: dict[str, Any]
+) -> None:
+    with requests.get(fastapi_openapi_url, timeout=10) as response:
+        response.raise_for_status()
+        openapi_json = json.loads(response.text)
+
+    openapi_oauth2_schema.pop("servers")
+    servers = openapi_json.pop("servers")
+
+    assert servers[0]["url"] == fastapi_openapi_url.split("/openapi.json")[0]
+    assert openapi_oauth2_schema == openapi_json
+
+
+@pytest.mark.parametrize(
+    "fastapi_openapi_url",
+    [(create_oauth2_fastapi_app)],
+    indirect=["fastapi_openapi_url"],
+)
+def test_generate_oauth2_client(fastapi_openapi_url: str) -> None:
+    api_client = OpenAPI.create(openapi_url=fastapi_openapi_url)
+    api_client.set_security_params(
+        OAuth2PasswordBearer.Parameters(bearer_token="token123")
+    )
+
+    expected = ["post_oauth_low_post"]
+
+    functions = list(api_client._get_functions_to_register())
+    assert [f.__name__ for f in functions] == expected
+
+    post_oauth_f = functions[0]
+
+    response = post_oauth_f(message="message")
+
+    assert response == {"message": "message"}
diff --git a/tests/api/openapi/test_uppercase_endpoint_parameters.py b/tests/api/openapi/test_uppercase_endpoint_parameters.py
index 2ffa3737..bfd627c3 100644
--- a/tests/api/openapi/test_uppercase_endpoint_parameters.py
+++ b/tests/api/openapi/test_uppercase_endpoint_parameters.py
@@ -207,11 +207,14 @@ def test_register_for_llm(
 
 
 @pytest.mark.azure_oai
+@pytest.mark.parametrize(
+    "fastapi_openapi_url", [(create_gify_fastapi_app)], indirect=["fastapi_openapi_url"]
+)
 def test_end2end(
-    gify_fastapi_openapi_url: str,
+    fastapi_openapi_url: str,
     azure_gpt35_turbo_16k_llm_config: dict[str, Any],
 ) -> None:
-    api = OpenAPI.create(openapi_url=gify_fastapi_openapi_url)
+    api = OpenAPI.create(openapi_url=fastapi_openapi_url)
 
     agent = ConversableAgent(name="agent", llm_config=azure_gpt35_turbo_16k_llm_config)
     user_proxy = UserProxyAgent(
diff --git a/tests/conftest.py b/tests/conftest.py
index 6a306521..6b451168 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -248,10 +248,10 @@ class Server(uvicorn.Server):  # type: ignore [misc]
 
 
 @pytest.fixture(scope="session")
-def fastapi_openapi_url() -> Iterator[str]:
+def fastapi_openapi_url(request: pytest.FixtureRequest) -> Iterator[str]:
     host = "127.0.0.1"
     port = find_free_port()
-    app = create_fastapi_app(host, port)
+    app = request.param(host, port)
     openapi_url = f"http://{host}:{port}/openapi.json"
 
     config = uvicorn.Config(app, host=host, port=port, log_level="info")
@@ -262,36 +262,6 @@ def fastapi_openapi_url() -> Iterator[str]:
         yield openapi_url
 
 
-@pytest.fixture(scope="session")
-def weather_fastapi_openapi_url() -> Iterator[str]:
-    host = "127.0.0.1"
-    port = find_free_port()
-    app = create_weather_fastapi_app(host, port)
-    openapi_url = f"http://{host}:{port}/openapi.json"
-
-    config = uvicorn.Config(app, host=host, port=port, log_level="info")
-    server = Server(config=config)
-    with server.run_in_thread():
-        time.sleep(1 if system() != "Windows" else 5)  # let the server start
-
-        yield openapi_url
-
-
-@pytest.fixture(scope="session")
-def gify_fastapi_openapi_url() -> Iterator[str]:
-    host = "127.0.0.1"
-    port = find_free_port()
-    app = create_gify_fastapi_app(host, port)
-
-    openapi_url = f"http://{host}:{port}/openapi.json"
-    config = uvicorn.Config(app, host=host, port=port, log_level="info")
-    server = Server(config=config)
-    with server.run_in_thread():
-        time.sleep(1 if system() != "Windows" else 5)  # let the server start
-
-        yield openapi_url
-
-
 @pytest.fixture
 def pydantic_version() -> float:
     return float(".".join(version_of_pydantic.split(".")[:2]))
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index dd35a4e6..a1fc7360 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,4 +1,6 @@
-from fastagency.helpers import jsonify_string
+from pathlib import Path
+
+from fastagency.helpers import jsonify_string, optional_temp_path
 
 
 def test_hello() -> None:
@@ -34,3 +36,12 @@ def test_bad_suggested_function_call() -> None:
 """
     actual = jsonify_string(content)
     assert actual == expected, actual
+
+
+def test_optional_temp_path() -> None:
+    with optional_temp_path() as temp_path:
+        assert temp_path.exists()
+    assert not temp_path.exists()
+
+    with optional_temp_path(path="defined_path") as temp_path:
+        assert temp_path == Path("defined_path")