Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add a single-file example #9

Merged
merged 1 commit into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ jobs:
# Download and cache dependencies
- restore_cache:
keys:
- v3-dependencies-{{ checksum "Pipfile.lock" }}
- v4-dependencies-{{ checksum "Pipfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v3-dependencies-
- v4-dependencies-

- run:
name: install dependencies
Expand All @@ -54,13 +54,19 @@ jobs:
- save_cache:
paths:
- ./.venv
key: v3-dependencies-{{ checksum "Pipfile.lock" }}
key: v4-dependencies-{{ checksum "Pipfile.lock" }}

- run:
name: run tests
command: |
make tests

- run:
name: run tests for the singlefile example
command: |
cd examples/singlefile
pipenv run python -m unittest app.py

release:
<<: *defaults

Expand All @@ -69,9 +75,9 @@ jobs:

- restore_cache:
keys:
- v3-dependencies-{{ checksum "Pipfile.lock" }}
- v4-dependencies-{{ checksum "Pipfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v3-dependencies-
- v4-dependencies-

- run:
name: install dependencies
Expand All @@ -84,7 +90,7 @@ jobs:
- save_cache:
paths:
- ./.venv
key: v3-dependencies-{{ checksum "Pipfile.lock" }}
key: v4-dependencies-{{ checksum "Pipfile.lock" }}

- run:
name: verify git tag vs. version
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pytest = "*"
pytest-cov = "*"
pytest-randomly = "*"
twine = "*"
webtest = "*"

[packages]
"pyramid-openapi3" = {editable = true, path = "."}
71 changes: 47 additions & 24 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions examples/singlefile/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""A single-file demo of pyramid_openapi3.

Usage:
* git clone https://github.com/niteoweb/pyramid_openapi3.git
* cd pyramid_openapi3/examples/singlefile
* virtualenv -p python3.7 .
* source bin/activate
* pip install pyramid_openapi3
* python app.py
"""

from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPForbidden
from pyramid.view import view_config
from wsgiref.simple_server import make_server

import tempfile
import unittest

# This is usually in a separate openapi.yaml file, but for the sake of the
# example we want everything in a single file. Other examples have it nicely
# separated.
OPENAPI_DOCUMENT = b"""
openapi: "3.0.0"
info:
version: "1.0.0"
title: Hello API
paths:
/hello:
get:
parameters:
- name: name
in: query
required: true
schema:
type: string
minLength: 3
responses:
200:
description: Say hello
"""


@view_config(route_name="hello", renderer="json", request_method="GET", openapi=True)
def hello(request):
"""Say hello."""
if request.openapi_validated.parameters["query"]["name"] == "admin":
raise HTTPForbidden()
return {"hello": request.openapi_validated.parameters["query"]["name"]}


def app(spec):
"""Prepare a Pyramid app."""
with Configurator() as config:
config.include("pyramid_openapi3")
config.pyramid_openapi3_spec(spec)
config.pyramid_openapi3_add_explorer()
config.add_route("hello", "/hello")
config.scan(".")
return config.make_wsgi_app()


if __name__ == "__main__":
"""If app.py is called directly, start up the app."""
with tempfile.NamedTemporaryFile() as document:
document.write(OPENAPI_DOCUMENT)
Copy link

Choose a reason for hiding this comment

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

Swagger client examples require the path to JSON spec https://github.com/swagger-api/swagger-js#import-in-browser. I think is better to expose JSON and yaml as routes, so that it can actually be used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

pyramid_openapi3 takes care of publishing the YAML file:

Screenshot 2019-04-14 at 20 08 27

I double checked, and the try-me examples seem to work:

Screenshot 2019-04-14 at 20 08 48

Or am I misunderstanding what you are trying to say?

Copy link

@dz0ny dz0ny Apr 14, 2019

Choose a reason for hiding this comment

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

I was thinking if you use API outside of the API explorer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, to convert YAML into JSON and publish both?

Copy link

@dz0ny dz0ny Apr 14, 2019

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's completely out of the scope of this PR.

#10

document.seek(0)

print("visit api explorer at http://0.0.0.0:6543/docs/") # noqa: T001
server = make_server("0.0.0.0", 6543, app(document.name))
server.serve_forever()


#######################################
# ---- Tests ---- #
# A couple of functional tests to #
# showcase pyramid_openapi3 features. #
# Usage: python -m unittest app.py #
#######################################

from openapi_core.schema.parameters.exceptions import InvalidParameterValue # noqa
from openapi_core.schema.parameters.exceptions import MissingRequiredParameter # noqa
from openapi_core.schema.responses.exceptions import InvalidResponse # noqa


class FunctionalTests(unittest.TestCase):
"""A suite of tests that make actual requests to a running app."""

def setUp(self):
"""Start up the app so that tests can send requests to it."""
from webtest import TestApp

with tempfile.NamedTemporaryFile() as document:
document.write(OPENAPI_DOCUMENT)
document.seek(0)

self.testapp = TestApp(app(document.name))

def test_nothing_on_root(self):
"""We have not configured our app to serve anything on root."""
res = self.testapp.get("/", status=404)
self.assertIn("404 Not Found", res.text)

def test_api_explorer_on_docs(self):
"""Swagger's API Explorer should be served on /docs/."""
res = self.testapp.get("/docs/", status=200)
self.assertIn("<title>Swagger UI</title>", res.text)

def test_hello(self):
"""Say hello."""
res = self.testapp.get("/hello?name=john", status=200)
self.assertIn('{"hello": "john"}', res.text)

def test_undefined_response(self):
"""Saying hello to admin should fail with 403 Forbidden.

But because we have not defined how a 403 response should look in
OPENAPI_DOCUMENT, we instead get a InvalidResponse exception.

This is to prevent us from forgetting to define all possible responses
in our opeanapi document.
"""
try:
self.testapp.get("/hello?name=admin", status=200)
except InvalidResponse as exc:
self.assertEqual(str(exc), "Unknown response http status: 403")

def test_name_missing(self):
"""Our view code does not even get called is request is not per-spec.

We don't have to write (and test!) any validation code in our view!
"""
try:
self.testapp.get("/hello", status=200)
except MissingRequiredParameter as exc:
self.assertEqual(str(exc), "Missing required parameter: name")

def test_name_too_short(self):
"""A name that is too short is picked up by openapi-core validation.

We don't have to write (and test!) any validation code in our view!
"""
try:
self.testapp.get("/hello?name=yo", status=200)
except InvalidParameterValue as exc:
self.assertEqual(
str(exc),
"Invalid parameter value for `name`: Value is shorter (2) "
"than the minimum length of 3",
)
2 changes: 1 addition & 1 deletion pyramid_openapi3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def wrapper_view(context, request):
)
else:
# Do the view
request.openapi_validated.raise_for_errors()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If the _pyramid_openapi3_validation_view_name view is not configured, we shouldn't just silently swallow validation errors and continue, but raise the appropriate validation exception.

response = view(context, request)

except HTTPException as exc:
Expand Down Expand Up @@ -151,7 +152,6 @@ def spec_view(request):

config.add_route(route_name, route)
config.add_view(route_name=route_name, view=spec_view)
config.add_view(route_name=route_name, view=spec_view)
Copy link
Collaborator Author

@zupo zupo Apr 14, 2019

Choose a reason for hiding this comment

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

Probably leftover of a rebase gone wrong.


custom_formatters = config.registry.settings.get("pyramid_openapi3_formatters")

Expand Down
24 changes: 12 additions & 12 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@
import pytest
import tempfile

MINIMAL_DOCUMENT = (
b'openapi: "3.0.0"\n'
b"info:\n"
b' version: "1.0.0"\n'
b" title: Foo API\n"
b"paths:\n"
b" /foo:\n"
b" get:\n"
b" responses:\n"
b" 200:\n"
b" description: A foo\n"
)
MINIMAL_DOCUMENT = b"""
openapi: "3.0.0"
info:
version: "1.0.0"
title: Foo API
paths:
/foo:
get:
responses:
200:
description: A foo
"""


def test_add_spec_view():
Expand Down