Skip to content

Commit

Permalink
Merge pull request #6 from sergeyklay/feature/rich-application
Browse files Browse the repository at this point in the history
Create  Modular Applications with Blueprints
  • Loading branch information
sergeyklay authored Feb 7, 2023
2 parents 71588bf + 4f6ddae commit 44ccc6c
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 44 deletions.
9 changes: 9 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

# Base URL for API provider.
BASE_URI=https://127.0.0.1:5000
3 changes: 1 addition & 2 deletions .github/workflows/contracts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ jobs:
pip install -r requirements.txt
- name: Run API server
run: flask run &
working-directory: src/products
run: flask --app runner:app run &
shell: bash

- name: Setup specmatic
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@

## How to try it out

### Install project dependencies
### Install dependencies and tools

First, install python dependencies:
First, install Python dependencies:

```bash
python3 -m pip install -r requirements.txt
```

Next, install [specmatic](https://specmatic.in/download/latest.html).
Next, Node.js linters and tools
```bash
npm install
```

Finally, install [specmatic](https://specmatic.in/download/latest.html).

### Run products API server
### Run API server

```bash
cd src/products
flask run &
npm run server
```

### Run the tests
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

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

15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
{
"name": "specmatic-testing-example",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"description": "Specmatic Testing Example",
"main": "index.js",
"scripts": {
"server": "flask --app runner:app run",
"lint-enforcer": "openapi-enforcer validate contracts/documentation.yaml",
"lint-redocly": "redocly lint --config redocly.yaml contracts/documentation.yaml",
"lint": "npm run lint-enforcer && npm run lint-redocly",
"all": "npm run lint"
"lint": "npm run lint-enforcer && npm run lint-redocly"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sergeyklay/specmatic-testing-example.git"
},
"keywords": [
"openapi",
"api",
"contracts",
"api-testing",
"contract-testing",
"contract-test",
"specmatic",
"setup"
"openapi3",
"openapi"
],
"author": "Serghei Iakovlev",
"license": "MIT",
Expand Down
File renamed without changes.
35 changes: 35 additions & 0 deletions provider/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

"""The top-level module for the application.
This module tracks the version of the application as well as the
base application info used by various functions within the
package and provides a factory function to create application
instance.
Misc variables:
__author__
__author_email__
__copyright__
__description__
__license__
__url__
__version__
"""

from flask import current_app

__copyright__ = 'Copyright (C) 2023 Serghei Iakovlev'
__version__ = '1.3.0'
__license__ = 'MIT'
__author__ = 'Serghei Iakovlev'
__author_email__ = '[email protected]'
__url__ = 'https://github.com/sergeyklay/specmatic-testing-example'
__description__ = 'Specmatic Testing Example'
14 changes: 14 additions & 0 deletions provider/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

"""The api blueprint module for the application."""

from flask import Blueprint

api = Blueprint('api', __name__)

from . import products
34 changes: 13 additions & 21 deletions src/products/app.py → provider/api/products.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
from flask import abort, json, Flask, request
from werkzeug.exceptions import HTTPException
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

from flask import abort, json, request, Response

app = Flask(__name__)

from provider import current_app
from provider.api import api

def get_products_list():
with open('products.json') as f:
return json.loads(f.read())


@app.errorhandler(HTTPException)
def resource_not_found(e):
"""Return JSON instead of HTML for HTTP errors."""
response = e.get_response()
response.data = json.dumps({
'code': e.code,
'name': e.name,
'description': e.description,
})
response.content_type = 'application/json'
return response


@app.route('/v1/products', methods=['GET'])
@api.route('/products', methods=['GET'])
def list():
data = get_products_list()
args = request.args
Expand All @@ -44,15 +36,15 @@ def list():
continue
products.append(p)

response = app.response_class(
response = current_app.response_class(
response=json.dumps(products),
status=200,
mimetype='application/json'
)
return response


@app.route('/v1/products/<id>', methods=['GET'])
@api.route('/products/<id>', methods=['GET'])
def get(id):
# This check is made on purpose to simulate 400 error
try:
Expand All @@ -63,7 +55,7 @@ def get(id):
data = get_products_list()
for product in data:
if product['id'] == id:
response = app.response_class(
response = current_app.response_class(
response=json.dumps(product),
status=200,
mimetype='application/json'
Expand Down
78 changes: 78 additions & 0 deletions provider/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

"""Manage the application creation and configuration process.
Functions:
create_app(config: str) -> flask.Flask
load_env_vars(base_path: str) -> None
configure_app(app: Flask, config_name=None) -> None
configure_blueprints(app: Flask) -> None
"""

import os

from flask import Flask
from werkzeug.exceptions import HTTPException


app = Flask(__name__)


def create_app(config=None) -> Flask:
"""Create and configure an instance of the Flask application."""
app = Flask(__name__)

configure_app(app, config)
configure_blueprints(app)

return app


def load_env_vars(base_path: str):
"""Load the current dotenv as system environment variable."""
dotenv_path = os.path.join(base_path, '.env')

from dotenv import load_dotenv
if os.path.exists(dotenv_path):
load_dotenv(dotenv_path=dotenv_path)


def configure_app(app: Flask, config_name=None):
"""Configure application."""
from provider.config import config, Config

# Use the default config and override it afterwards
app.config.from_object(config['default'])

if config is not None:
# Config name as a string
if isinstance(config_name, str) and config_name in config:
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Config as an object
else:
app.config.from_object(config_name)
if isinstance(config_name, Config):
config_name.init_app(app)

# Update config from environment variable (if any).
# Export this variable as follows:
# export APP_SETTINGS="/var/www/server/config.py"
app.config.from_envvar('APP_SETTINGS', silent=True)


def configure_blueprints(app: Flask):
"""Configure blueprints for the application."""
# main blueprint registration
from provider.main import main as main_blueprint
app.register_blueprint(main_blueprint)

# api blueprint registration
from provider.api import api as api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/v1')
36 changes: 36 additions & 0 deletions provider/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

import os


class Config:
BASE_PATH = os.path.abspath(os.path.dirname(__file__))

@staticmethod
def init_app(app):
pass


class DevelopmentConfig(Config):
DEBUG = True


class TestingConfig(Config):
TESTING = True


class ProductionConfig(Config):
pass


config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig,
}
19 changes: 19 additions & 0 deletions provider/main/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This file is part of the Specmatic Testing Example.
#
# Copyright (C) 2023 Serghei Iakovlev <[email protected]>
#
# For the full copyright and license information, please view
# the LICENSE file that was distributed with this source code.

"""The main blueprint module for the application.
Provides the routes and errors handlers definition for the
application.
"""

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors
Loading

0 comments on commit 44ccc6c

Please sign in to comment.