• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

playpauseandstop / rororo / 3307165710

pending completion
3307165710

push

github

GitHub
build: Update Python dev version to 3.10.8 (#293)

368 of 384 branches covered (95.83%)

15 of 15 new or added lines in 4 files covered. (100.0%)

1077 of 1096 relevant lines covered (98.27%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.25
/src/rororo/openapi/openapi.py
1
import inspect
1✔
2
import json
1✔
3
import os
1✔
4
import warnings
1✔
5
from functools import lru_cache, partial
1✔
6
from pathlib import Path
1✔
7
from typing import (
1✔
8
    Callable,
9
    cast,
10
    Deque,
11
    Dict,
12
    List,
13
    Optional,
14
    overload,
15
    Tuple,
16
    Union,
17
)
18

19
import attr
1✔
20
import yaml
1✔
21
from aiohttp import hdrs, web
1✔
22
from aiohttp_middlewares import cors_middleware
1✔
23
from aiohttp_middlewares.annotations import (
1✔
24
    ExceptionType,
25
    StrCollection,
26
    UrlCollection,
27
)
28
from aiohttp_middlewares.error import Config as ErrorMiddlewareConfig
1✔
29
from openapi_core.schema.specs.models import Spec
1✔
30
from openapi_core.shortcuts import create_spec
1✔
31
from pyrsistent import pmap
1✔
32
from yarl import URL
1✔
33

34
from rororo.annotations import (
1✔
35
    DictStrAny,
36
    DictStrStr,
37
    F,
38
    Handler,
39
    Protocol,
40
    TypedDict,
41
    ViewType,
42
)
43
from rororo.openapi import views
1✔
44
from rororo.openapi.annotations import SecurityDict, ValidateEmailKwargsDict
1✔
45
from rororo.openapi.constants import (
1✔
46
    APP_OPENAPI_SCHEMA_KEY,
47
    APP_OPENAPI_SPEC_KEY,
48
    APP_VALIDATE_EMAIL_KWARGS_KEY,
49
    HANDLER_OPENAPI_MAPPING_KEY,
50
)
51
from rororo.openapi.core_data import get_core_operation
1✔
52
from rororo.openapi.exceptions import ConfigurationError
1✔
53
from rororo.openapi.middlewares import openapi_middleware
1✔
54
from rororo.openapi.utils import add_prefix
1✔
55
from rororo.settings import APP_SETTINGS_KEY, BaseSettings
1✔
56

57

58
SchemaLoader = Callable[[bytes], DictStrAny]
1✔
59
Url = Union[str, URL]
1✔
60

61

62
class CorsMiddlewareKwargsDict(TypedDict, total=False):
1✔
63
    allow_all: bool
1✔
64
    origins: Optional[UrlCollection]
1✔
65
    urls: Optional[UrlCollection]
1✔
66
    expose_headers: Optional[UrlCollection]
1✔
67
    allow_headers: StrCollection
1✔
68
    allow_methods: StrCollection
1✔
69
    allow_credentials: bool
1✔
70
    max_age: Optional[int]
1✔
71

72

73
class CreateSchemaAndSpec(Protocol):
1✔
74
    def __call__(
1✔
75
        self, path: Path, *, schema_loader: SchemaLoader = None
76
    ) -> Tuple[DictStrAny, Spec]:  # pragma: no cover
77
        ...
×
78

79

80
class ErrorMiddlewareKwargsDict(TypedDict, total=False):
1✔
81
    default_handler: Handler
1✔
82
    config: Optional[ErrorMiddlewareConfig]
1✔
83
    ignore_exceptions: Optional[
1✔
84
        Union[ExceptionType, Tuple[ExceptionType, ...]]
85
    ]
86

87

88
@attr.dataclass(slots=True)
1✔
89
class OperationTableDef:
1✔
90
    """Map OpenAPI 3 operations to aiohttp.web view handlers.
91

92
    In short it is rororo's equialent to :class:`aiohttp.web.RouteTableDef`.
93
    Under the hood, on :func:`rororo.openapi.setup_openapi` it
94
    still will use ``RouteTableDef`` for registering view handlers to
95
    :class:`aiohttp.web.Application`.
96

97
    But unlike ``RouteTableDef`` it does not register any HTTP method handlers
98
    (as via ``@routes.get`` decorator) in favor of just registering the
99
    operations.
100

101
    There are two ways for registering view hanlder,
102

103
    1. With bare ``@operations.register`` call when OpenAPI ``operationId``
104
       equals to view handler name.
105
    2. Or by passing ``operation_id`` to the decorator as first arg, when
106
       ``operationId`` does not match view handler name, or if you don't like
107
       the fact of guessing operation ID from view handler name.
108

109
    Both of ways described below,
110

111
    .. code-block:: python
112

113
        from rororo import OperationTableDef
114

115
        operations = OperationTableDef()
116

117
        # Expect OpenAPI 3 schema to contain operationId: hello_world
118
        @operations.register
119
        async def hello_world(request: web.Request) -> web.Response:
120
            ...
121

122

123
        # Explicitly use `operationId: "helloWorld"`
124
        @operations.register("helloWorld")
125
        async def hello_world(request: web.Request) -> web.Response:
126
            ...
127

128
    Class based views supported as well. In most generic way you just need
129
    to decorate your view with ``@operations.register`` decorator and ensure
130
    that ``operationId`` equals to view method qualified name
131
    (``__qualname__``).
132

133
    For example,
134

135
    .. code-block:: python
136

137
        @operations.register
138
        class UserView(web.View):
139
            async def get(self) -> web.Response:
140
                ...
141

142
    expects for operation ID ``UserView.get`` to be declared in OpenAPI schema.
143

144
    In same time,
145

146
    .. code-block:: python
147

148
        @operations.register("users")
149
        class UserView(web.View):
150
            async def get(self) -> web.Response:
151
                ...
152

153
    expects for operation ID ``users.get`` to be declared in OpenAPI schema.
154

155
    Finally,
156

157
    .. code-block:: python
158

159
        @operations.register
160
        class UserView(web.View):
161
            @operations.register("me")
162
            async def get(self) -> web.Response:
163
                ...
164

165
    expects for operation ID ``me`` to be declared in OpenAPI schema.
166

167
    When the class based view provides mutliple view methods (for example
168
    ``delete``, ``get``, ``patch`` & ``put``) *rororo* expects that
169
    OpenAPI schema contains operation IDs for each of view method.
170

171
    If supplied ``operation_id`` does not exist in OpenAPI 3 schema,
172
    :func:`rororo.openapi.setup_openapi` call raises an ``OperationError``.
173
    """
174

175
    handlers: List[Handler] = attr.Factory(list)
1✔
176
    views: List[ViewType] = attr.Factory(list)
1✔
177

178
    def __add__(self, other: "OperationTableDef") -> "OperationTableDef":
1✔
179
        return OperationTableDef(
1✔
180
            handlers=[*self.handlers, *other.handlers],
181
            views=[*self.views, *other.views],
182
        )
183

184
    def __iadd__(self, other: "OperationTableDef") -> "OperationTableDef":
1✔
185
        self.handlers.extend(other.handlers)
1✔
186
        self.views.extend(other.views)
1✔
187
        return self
1✔
188

189
    @overload
190
    def register(self, handler: F) -> F:
191
        ...
192

193
    @overload
194
    def register(self, operation_id: str) -> Callable[[F], F]:
195
        ...
196

197
    def register(self, mixed):  # type: ignore[no-untyped-def]
1✔
198
        operation_id = mixed if isinstance(mixed, str) else mixed.__qualname__
1✔
199

200
        def decorator(handler: F) -> F:
1✔
201
            mapping: DictStrStr = {}
1✔
202

203
            if self._is_view(handler):
1✔
204
                mapping.update(
1✔
205
                    self._register_view(handler, operation_id)  # type: ignore[arg-type]
206
                )
207
            else:
208
                mapping.update(self._register_handler(handler, operation_id))
1✔
209

210
            setattr(handler, HANDLER_OPENAPI_MAPPING_KEY, pmap(mapping))
1✔
211
            return handler
1✔
212

213
        return decorator(mixed) if callable(mixed) else decorator
1✔
214

215
    def _is_view(self, handler: F) -> bool:
1✔
216
        return inspect.isclass(handler) and issubclass(handler, web.View)
1✔
217

218
    def _register_handler(
1✔
219
        self, handler: Handler, operation_id: str
220
    ) -> DictStrStr:
221
        # Hacky way to check whether handler is a view function or view method
222
        has_self_parameter = "self" in inspect.signature(handler).parameters
1✔
223

224
        # Register only view functions, view methods will be registered via
225
        # view class instead
226
        if not has_self_parameter:
1✔
227
            self.handlers.append(handler)
1✔
228

229
        return {hdrs.METH_ANY: operation_id}
1✔
230

231
    def _register_view(self, view: ViewType, prefix: str) -> DictStrStr:
1✔
232
        mapping: DictStrStr = {}
1✔
233

234
        for value in vars(view).values():
1✔
235
            if not callable(value):
1✔
236
                continue
1✔
237

238
            name = value.__name__
1✔
239
            maybe_method = name.upper()
1✔
240
            if maybe_method not in hdrs.METH_ALL:
1✔
241
                continue
1✔
242

243
            maybe_operation_id = getattr(
1✔
244
                value, HANDLER_OPENAPI_MAPPING_KEY, {}
245
            ).get(hdrs.METH_ANY)
246
            mapping[maybe_method] = (
1✔
247
                maybe_operation_id
248
                if maybe_operation_id
249
                else f"{prefix}.{name}"
250
            )
251

252
        self.views.append(view)
1✔
253
        return mapping
1✔
254

255

256
def convert_operations_to_routes(
1✔
257
    operations: OperationTableDef, spec: Spec, *, prefix: str = None
258
) -> web.RouteTableDef:
259
    """Convert operations table defintion to routes table definition."""
260

261
    async def noop(request: web.Request) -> web.Response:
1✔
262
        return web.json_response(status=204)  # pragma: no cover
×
263

264
    routes = web.RouteTableDef()
1✔
265

266
    # Add plain handlers to the route table def as a route
267
    for handler in operations.handlers:
1✔
268
        operation_id = getattr(handler, HANDLER_OPENAPI_MAPPING_KEY)[
1✔
269
            hdrs.METH_ANY
270
        ]
271
        core_operation = get_core_operation(spec, operation_id)
1✔
272

273
        routes.route(
1✔
274
            core_operation.http_method,
275
            add_prefix(core_operation.path_name, prefix),
276
            name=get_route_name(core_operation.operation_id),
277
        )(handler)
278

279
    # But view should be added as a view instead
280
    for view in operations.views:
1✔
281
        ids: Deque[str] = Deque(
1✔
282
            getattr(view, HANDLER_OPENAPI_MAPPING_KEY).values()
283
        )
284

285
        first_operation_id = ids.popleft()
1✔
286
        core_operation = get_core_operation(spec, first_operation_id)
1✔
287

288
        path = add_prefix(core_operation.path_name, prefix)
1✔
289
        routes.view(
1✔
290
            path,
291
            name=get_route_name(core_operation.operation_id),
292
        )(view)
293

294
        # Hacky way of adding aliases to class based views with multiple
295
        # registered view methods
296
        for other_operation_id in ids:
1✔
297
            routes.route(
1✔
298
                hdrs.METH_ANY, path, name=get_route_name(other_operation_id)
299
            )(noop)
300

301
    return routes
1✔
302

303

304
def create_schema_and_spec(
1✔
305
    path: Path, *, schema_loader: SchemaLoader = None
306
) -> Tuple[DictStrAny, Spec]:
307
    schema = read_openapi_schema(path, loader=schema_loader)
1✔
308
    return (schema, create_spec(schema))
1✔
309

310

311
@lru_cache(maxsize=128)
1✔
312
def create_schema_and_spec_with_cache(  # type: ignore[misc]
1✔
313
    path: Path, *, schema_loader: SchemaLoader = None
314
) -> Tuple[DictStrAny, Spec]:
315
    return create_schema_and_spec(path, schema_loader=schema_loader)
1✔
316

317

318
def find_route_prefix(
1✔
319
    oas: DictStrAny,
320
    *,
321
    server_url: Union[str, URL] = None,
322
    settings: BaseSettings = None,
323
) -> str:
324
    if server_url is not None:
1✔
325
        return get_route_prefix(server_url)
1✔
326

327
    servers = oas["servers"]
1✔
328
    if len(servers) == 1:
1✔
329
        return get_route_prefix(servers[0]["url"])
1✔
330

331
    if settings is None:
1✔
332
        raise ConfigurationError(
1✔
333
            "Unable to guess route prefix as OpenAPI schema contains "
334
            "multiple servers and aiohttp.web has no settings instance "
335
            "configured."
336
        )
337

338
    for server in servers:
1✔
339
        mixed = server.get("x-rororo-level")
1✔
340
        if isinstance(mixed, list):
1✔
341
            if settings.level in mixed:
1✔
342
                return get_route_prefix(server["url"])
1✔
343
        elif mixed == settings.level:
1✔
344
            return get_route_prefix(server["url"])
1✔
345

346
    raise ConfigurationError(
1✔
347
        "Unable to guess route prefix as no server in OpenAPI schema has "
348
        f'defined "x-rororo-level" key of "{settings.level}".'
349
    )
350

351

352
def fix_spec_operations(spec: Spec, schema: DictStrAny) -> Spec:
1✔
353
    """Fix spec operations.
354

355
    ``openapi-core`` sets up operation security to an empty list even it is
356
    not defined within the operation schema. This function fixes this behaviour
357
    by reading schema first and if operation schema misses ``security``
358
    definition - sets up a ``None`` as an operation security.
359

360
    This allows properly distinct empty operation security and missed operation
361
    security. With empty operation security (empty list) - mark an operation
362
    as unsecured. With missed operation security - use global security schema
363
    if it is defined.
364
    """
365
    mapping: Dict[str, Optional[SecurityDict]] = {}
1✔
366

367
    for path_data in schema["paths"].values():
1✔
368
        for maybe_operation_data in path_data.values():
1✔
369
            if not isinstance(maybe_operation_data, dict):
1✔
370
                continue
1✔
371

372
            operation_id = maybe_operation_data.get("operationId")
1✔
373
            if operation_id is None:
1✔
374
                continue
1✔
375

376
            mapping[operation_id] = maybe_operation_data.get("security")
1✔
377

378
    for path in spec.paths.values():
1✔
379
        for operation in path.operations.values():
1✔
380
            if operation.security != []:
1✔
381
                continue
1✔
382

383
            if operation.operation_id is None:
1✔
384
                continue
1✔
385

386
            operation.security = mapping[operation.operation_id]
1✔
387

388
    return spec
1✔
389

390

391
def get_default_yaml_loader() -> SchemaLoader:
1✔
392
    return cast(SchemaLoader, getattr(yaml, "CSafeLoader", yaml.SafeLoader))
1✔
393

394

395
def get_route_name(operation_id: str) -> str:
1✔
396
    return operation_id.replace(" ", "-")
1✔
397

398

399
def get_route_prefix(mixed: Url) -> str:
1✔
400
    return (URL(mixed) if isinstance(mixed, str) else mixed).path
1✔
401

402

403
def read_openapi_schema(
1✔
404
    path: Path, *, loader: SchemaLoader = None
405
) -> DictStrAny:
406
    """Read OpenAPI Schema from given path.
407

408
    By default, when ``loader`` is not explicitly passed, attempt to guess
409
    schema loader function from path extension.
410

411
    ``loader`` should be a callable, which receives ``bytes`` and returns
412
    ``Dict[str, Any]`` of OpenAPI Schema.
413

414
    By default, next schema loader used,
415

416
    - :func:`json.loads` for ``openapi.json``
417
    - ``yaml.load`` for ``openapi.yaml``
418
    """
419
    if loader is None:
1✔
420
        if path.suffix == ".json":
1✔
421
            loader = json.loads
1✔
422
        elif path.suffix in {".yml", ".yaml"}:
1✔
423
            loader = partial(yaml.load, Loader=get_default_yaml_loader())
1✔
424

425
    if loader is not None:
1✔
426
        return loader(path.read_bytes())
1✔
427

428
    raise ConfigurationError(
1✔
429
        f"Unsupported OpenAPI schema file: {path}. At a moment rororo "
430
        "supports loading OpenAPI schemas from: .json, .yml, .yaml files"
431
    )
432

433

434
@overload
435
def setup_openapi(
436
    app: web.Application,
437
    schema_path: Union[str, Path],
438
    *operations: OperationTableDef,
439
    server_url: Url = None,
440
    is_validate_response: bool = True,
441
    has_openapi_schema_handler: bool = True,
442
    use_error_middleware: bool = True,
443
    error_middleware_kwargs: ErrorMiddlewareKwargsDict = None,
444
    use_cors_middleware: bool = True,
445
    cors_middleware_kwargs: CorsMiddlewareKwargsDict = None,
446
    schema_loader: SchemaLoader = None,
447
    cache_create_schema_and_spec: bool = False,
448
    validate_email_kwargs: ValidateEmailKwargsDict = None,
449
) -> web.Application:
450
    ...
451

452

453
@overload
454
def setup_openapi(
455
    app: web.Application,
456
    *operations: OperationTableDef,
457
    schema: DictStrAny,
458
    spec: Spec,
459
    server_url: Url = None,
460
    is_validate_response: bool = True,
461
    has_openapi_schema_handler: bool = True,
462
    use_error_middleware: bool = True,
463
    error_middleware_kwargs: ErrorMiddlewareKwargsDict = None,
464
    use_cors_middleware: bool = True,
465
    cors_middleware_kwargs: CorsMiddlewareKwargsDict = None,
466
    validate_email_kwargs: ValidateEmailKwargsDict = None,
467
) -> web.Application:
468
    ...
469

470

471
def setup_openapi(  # type: ignore[misc]
1✔
472
    app: web.Application,
473
    schema_path: Union[str, Path] = None,
474
    *operations: OperationTableDef,
475
    schema: DictStrAny = None,
476
    spec: Spec = None,
477
    server_url: Url = None,
478
    is_validate_response: bool = True,
479
    has_openapi_schema_handler: bool = True,
480
    use_error_middleware: bool = True,
481
    error_middleware_kwargs: DictStrAny = None,
482
    use_cors_middleware: bool = True,
483
    cors_middleware_kwargs: DictStrAny = None,
484
    schema_loader: SchemaLoader = None,
485
    cache_create_schema_and_spec: bool = False,
486
    validate_email_kwargs: ValidateEmailKwargsDict = None,
487
) -> web.Application:
488
    """Setup OpenAPI schema to use with aiohttp.web application.
489

490
    Unlike `aiohttp-apispec <https://aiohttp-apispec.readthedocs.io/>`_ and
491
    other tools, which provides OpenAPI/Swagger support for aiohttp.web
492
    applications, *rororo* changes the way of using OpenAPI schema with
493
    ``aiohttp.web`` apps.
494

495
    *rororo* using schema first approach and relies on concrete OpenAPI schema
496
    file, path to which need to be registered on application startup (mostly
497
    inside of ``create_app`` factory or right after
498
    :class:`aiohttp.web.Application` instantiation).
499

500
    And as valid OpenAPI schema ensure unique ``operationId`` used accross the
501
    schema *rororo* uses them as a key while telling aiohttp.web to use given
502
    view handler for serving required operation.
503

504
    With that in mind registering (setting up) OpenAPI schema requires:
505

506
    1. :class:`aiohttp.web.Application` instance
507
    2. Path to file (json or yaml) with OpenAPI schema
508
    3. OpenAPI operation handlers mapping (rororo's equialent of
509
       :class:`aiohttp.web.RouteTableDef`)
510

511
    In common cases setup looks like,
512

513
    .. code-block:: python
514

515
        from pathlib import Path
516
        from typing import List
517

518
        from aiohttp import web
519

520
        from .views import operations
521

522

523
        def create_app(argv: List[str] = None) -> web.Application:
524
            return setup_openapi(
525
                web.Application(),
526
                Path(__file__).parent / "openapi.yaml",
527
                operations,
528
            )
529

530
    If your OpenAPI schema contains multiple servers schemas, like,
531

532
    .. code-block:: yaml
533

534
        servers:
535
        - url: "/api/"
536
          description: "Test environment"
537
        - url: "http://localhost:8080/api/"
538
          description: "Dev environment"
539
        - url: "http://prod.url/api/"
540
          description: "Prod environment"
541

542
    you have 2 options of telling *rororo* how to use specific server URL.
543

544
    First, is passing ``server_url``, while setting up OpenAPI, for example,
545

546
    .. code-block:: python
547

548
        setup_openapi(
549
            web.Application(),
550
            Path(__file__).parent / "openapi.yaml",
551
            operations,
552
            server_url=URL("http://prod.url/api/"),
553
        )
554

555
    Second, is more complicated as you need to wrap ``aiohttp.web`` application
556
    into :func:`rororo.settings.setup_settings` and mark each server with
557
    ``x-rororo-level`` special key in server schema definition as,
558

559
    .. code-block:: yaml
560

561
        servers:
562
        - url: "/api/"
563
          x-rororo-level: "test"
564
        - url: "http://localhost:8080/api/"
565
          x-rororo-level: "dev"
566
        - url: "http://prod.url/api/"
567
          x-rororo-level: "prod"
568

569
    After, *rororo* will try to equal current app settings level with the
570
    schema and if URL matched, will use given server URL for finding out
571
    route prefix.
572

573
    By default, *rororo* will validate operation responses against OpenAPI
574
    schema. To disable this feature, pass ``is_validate_response`` falsy flag.
575

576
    By default, *rororo* will share the OpenAPI schema which is registered
577
    for your aiohttp.web application. In case if you don't want to share this
578
    schema, pass ``has_openapi_schema_handler=False`` on setting up OpenAPI.
579

580
    By default, *rororo* will enable
581
    :func:`aiohttp_middlewares.cors.cors_middleware` without any settings and
582
    :func:`aiohttp_middlewares.error.error_middleware` with custom error
583
    handler to ensure that security / validation errors does not provide any
584
    mess to stdout. Pass ``use_cors_middleware`` /
585
    ``use_error_middleware`` to change or entirely disable this default
586
    behaviour.
587

588
    For passing custom options to CORS middleware, use
589
    ``cors_middleware_kwargs`` mapping. If kwarg does not support by CORS
590
    middleware - *rororo* will raise a ``ConfigurationError``. All list of
591
    options available at documentation for
592
    :func:`aiohttp_middlewares.cors.cors_middleware`.
593

594
    To simplify things *rororo* expects on OpenAPI 3 path and do reading schema
595
    from file and specifying ``openapi_core.schema.specs.models.Spec`` instance
596
    inside of :func:`rororo.openapi.setup_openapi` call.
597

598
    However, it is possible to completely customize this default behaviour and
599
    pass OpenAPI ``schema`` and ``spec`` instance directly. In that case
600
    ``schema`` keyword argument should contains raw OpenAPI 3 schema as
601
    ``Dict[str, Any]``, while ``spec`` to be an
602
    ``openapi_core.schema.specs.models.Spec`` instance.
603

604
    This behaviour might be helpful if you'd like to cache reading schema and
605
    instantiating spec within tests or other environments, which requires
606
    multiple :func:`rororo.openapi.setup_openapi` calls.
607

608
    .. code-block:: python
609

610
        from pathlib import Path
611

612
        import yaml
613
        from aiohttp import web
614
        from openapi_core.shortcuts import create_spec
615
        from rororo import setup_openapi
616

617

618
        # Reusable OpenAPI data
619
        openapi_yaml = Path(__file__).parent / "openapi.yaml"
620
        schema = yaml.load(
621
            openapi_yaml.read_bytes(), Loader=yaml.CSafeLoader
622
        )
623
        spec = create_spec(schema)
624

625
        # Create OpenAPI 3 aiohttp.web server application
626
        app = setup_openapi(web.Application(), schema=schema, spec=spec)
627

628
    For default behaviour, with passing ``schema_path``, there are few options
629
    on customizing schema load process as well,
630

631
    By default, *rororo* will use :func:`json.loads` to load OpenAPI schema
632
    content from JSON file and ``yaml.CSafeLoader`` if it is available to load
633
    schema content from YAML files (with fallback to ``yaml.SafeLoader``). But,
634
    for performance considreations, you might use any other function to load
635
    the schema. Example below illustrates how to use ``ujson.loads`` function
636
    to load content from JSON schema,
637

638
    .. code-block:: python
639

640
        import ujson
641

642
        app = setup_openapi(
643
            web.Application(),
644
            Path(__file__).parent / "openapi.json",
645
            operations,
646
            schema_loader=ujson.loads,
647
        )
648

649
    Schema loader function expects ``bytes`` as only argument and should return
650
    ``Dict[str, Any]`` as OpenAPI schema dict.
651

652
    .. danger::
653
        By default *rororo* does not cache slow calls to read OpenAPI schema
654
        and creating its spec. But sometimes, for example in tests, it is
655
        sufficient to cache those calls. To enable cache behaviour pass
656
        ``cache_create_schema_and_spec=True`` or even better,
657
        ``cache_create_schema_and_spec=settings.is_test``.
658

659
        But this may result in unexpected issues, as schema and spec will be
660
        cached once and on next call it will result cached data instead to
661
        attempt read fresh schema from the disk and instantiate OpenAPI Spec
662
        instance.
663

664
    By default, *rororo* using ``validate_email`` function from
665
    `email-validator <https://github.com/JoshData/python-email-validator>`_
666
    library to validate email strings, which has been declared in OpenAPI
667
    schema as,
668

669
    .. code-block:: yaml
670

671
        components:
672
          schemas:
673
            Email:
674
              type: "string"
675
              format: "email"
676

677
    In most cases ``validate_email(email)`` call should be enough, but in case
678
    if you need to pass extra ``**kwargs`` for validating email strings, setup
679
    ``validate_email_kwargs`` such as,
680

681
    .. code-block:: python
682

683
        app = setup_openapi(
684
            web.Application(),
685
            Path(__file__).parent / "openapi.json",
686
            operations,
687
            validate_email_kwargs={"check_deliverability": False},
688
        )
689

690
    """
691

692
    if isinstance(schema_path, OperationTableDef):
1✔
693
        operations = (schema_path, *operations)
1✔
694
        schema_path = None
1✔
695

696
    if schema is None and spec is None:
1✔
697
        if schema_path is None:
1✔
698
            raise ConfigurationError(
1✔
699
                "Please supply only `spec` keyword argument, or only "
700
                "`schema_path` positional argument, not both."
701
            )
702

703
        # Ensure OpenAPI schema is a readable file
704
        path = (
1✔
705
            Path(schema_path) if isinstance(schema_path, str) else schema_path
706
        )
707
        if not path.exists() or not path.is_file():
1✔
708
            uid = os.getuid()
1✔
709
            raise ConfigurationError(
1✔
710
                f"Unable to find OpenAPI schema file at {path}. Please check "
711
                "that file exists at given path and readable by current user "
712
                f"ID: {uid}"
713
            )
714

715
        # Create the spec and put it to the application dict as well
716
        create_func: CreateSchemaAndSpec = (
1✔
717
            create_schema_and_spec_with_cache  # type: ignore[assignment]
718
            if cache_create_schema_and_spec
719
            else create_schema_and_spec
720
        )
721

722
        try:
1✔
723
            schema, spec = create_func(path, schema_loader=schema_loader)
1✔
724
        except Exception:
1✔
725
            raise ConfigurationError(
1✔
726
                f"Unable to load valid OpenAPI schema in {path}. In most "
727
                "cases it means that given file doesn't contain valid OpenAPI "
728
                "3 schema. To get full details about errors run "
729
                f"`openapi-spec-validator {path.absolute()}`"
730
            )
731
    elif schema_path is not None:
1✔
732
        warnings.warn(
1✔
733
            "You supplied `schema_path` positional argument as well as "
734
            "supplying `schema` & `spec` keyword arguments. `schema_path` "
735
            "will be ignored in favor of `schema` & `spec` args."
736
        )
737

738
    # Fix all operation securities within OpenAPI spec
739
    spec = fix_spec_operations(spec, cast(DictStrAny, schema))
1✔
740

741
    # Store schema, spec, and validate email kwargs in application dict
742
    app[APP_OPENAPI_SCHEMA_KEY] = schema
1✔
743
    app[APP_OPENAPI_SPEC_KEY] = spec
1✔
744
    app[APP_VALIDATE_EMAIL_KWARGS_KEY] = validate_email_kwargs
1✔
745

746
    # Register the route to dump openapi schema used for the application if
747
    # required
748
    route_prefix = find_route_prefix(
1✔
749
        cast(DictStrAny, schema),
750
        server_url=server_url,
751
        settings=app.get(APP_SETTINGS_KEY),
752
    )
753
    if has_openapi_schema_handler:
1✔
754
        app.router.add_get(
1✔
755
            add_prefix("/openapi.{schema_format}", route_prefix),
756
            views.openapi_schema,
757
        )
758

759
    # Register all operation handlers to web application
760
    for item in operations:
1✔
761
        app.router.add_routes(
1✔
762
            convert_operations_to_routes(item, spec, prefix=route_prefix)
763
        )
764

765
    # Add OpenAPI middleware
766
    kwargs = error_middleware_kwargs or {}
1✔
767
    kwargs.setdefault("default_handler", views.default_error_handler)
1✔
768

769
    try:
1✔
770
        app.middlewares.insert(
1✔
771
            0,
772
            openapi_middleware(
773
                is_validate_response=is_validate_response,
774
                use_error_middleware=use_error_middleware,
775
                error_middleware_kwargs=kwargs,
776
            ),
777
        )
778
    except TypeError:
1✔
779
        raise ConfigurationError(
1✔
780
            "Unsupported kwargs passed to error middleware. Please check "
781
            "given kwargs and remove unsupported ones: "
782
            f"{error_middleware_kwargs!r}"
783
        )
784

785
    # Add CORS middleware if necessary
786
    if use_cors_middleware:
1✔
787
        try:
1✔
788
            app.middlewares.insert(
1✔
789
                0, cors_middleware(**(cors_middleware_kwargs or {}))
790
            )
791
        except TypeError:
1✔
792
            raise ConfigurationError(
1✔
793
                "Unsupported kwargs passed to CORS middleware. Please check "
794
                "given kwargs and remove unsupported ones: "
795
                f"{cors_middleware_kwargs!r}"
796
            )
797

798
    return app
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc