Usage

To use Connexion Sqlalchemy Utils in a project:

import connexion_sql_utils

This package is meant to be used in conjunction with Connexion which utilizes an API first approach to building REST API’s for micro-services.

This package has an Sqlalchemy Mixin used with Postgresql to create a declarative base, which can then be used to declare your database models.

This package also has a set of utility functions that when combined with functools.partial can be used to quickly create the routes for the api.

Connexion Sqlalchemy Utils Example

By cloning the repo:

git clone https://github.com/m-housh/connexion_sql_utils.git

cd ./connexion_sql_utils

docker-compose up

Without cloning the repo:

docker pull mhoush/connexion_sql_utils
docker pull postgres/alpine

docker run -d --name some_postgres \
    -e POSTGRES_PASSWORD=postgres \
    postgres:alpine

docker run --rm -it --link some_postgres:postgres \
    -e DB_HOST=postgres \
    -e DB_PASSWORD=postgres \
    -p "8080:8080" \
    mhoush/connexion_sql_utils

Check out the example api at http://localhost:8080/ui

app.py:

#!/usr/bin/env python

import os
from functools import partial
import logging

import connexion

from sqlalchemy import Column, String, Numeric, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

from connexion_sql_utils import BaseMixin, to_json, event_func, dump_method
from connexion_sql_utils import crud

# Most of this would typically be in a different module, but since
# this is just an example, I'm sticking it all into this module.

DB_USER = os.environ.get('DB_USER', 'postgres')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'postgres')
DB_HOST = os.environ.get('DB_HOST', 'localhost')
DB_PORT = os.environ.get('DB_PORT', 5432)
DB_NAME = os.environ.get('DB_NAME', 'postgres')


DB_URI = 'postgres+psycopg2://{user}:{password}@{host}:{port}/{db}'.format(
    user=DB_USER,
    password=DB_PASSWORD,
    host=DB_HOST,
    port=DB_PORT,
    db=DB_NAME
)

engine = create_engine(DB_URI)

Session = scoped_session(
    sessionmaker(autocommit=False, autoflush=False, bind=engine,
                 expire_on_commit=False)
)


# The only method required to complete the mixin is to add a staticmethod
# that should return a session.  This is used in the queries.
class MyBase(BaseMixin):

    # give the models an access to a session.
    @staticmethod
    def session_maker():
        return Session()


# By attaching the ``session_maker`` method to the class we now create a
# ``declarative_base`` to be used.  The ``BaseMixin`` class declares an
# ``id`` column, that is ``postgresql.UUID``.  It also has an declared attr
# for the __tablename__.  If you would to override these, they can be declared
# when create your database model
DbModel = declarative_base(cls=MyBase)


class Foo(DbModel):

    bar = Column(String(40), nullable=False)
    baz = Column(Numeric, nullable=True)

    # a method to be called to help in the conversion to json.
    @to_json('baz')
    def convert_decimal(self, val):
        if val is not None:
            logging.debug('Converting baz...')
            return float(val)
        return val

    # add a custom value when dumping to json
    @dump_method
    def add_bang(self, vals):
        logging.debug('adding bang')
        vals['bang'] = 'boom'
        return vals

    # attach an event listener to ensure ``bar`` is only saved
    # as a lower case string.
    @staticmethod
    @event_func('before_insert', 'before_update')
    def lower_baz(mapper, connection, target):
        target.bar = str(target.bar).lower()


# CRUD methods used in ``opertionId`` field of ``swagger.yml``
# connexion needs named parameters in it's operitionId field, so you must
# declare them in the partial in order to work correctly.
get_foo = partial(crud.get, Foo, limit=None, bar=None)
post_foo = partial(crud.post, Foo, foo=None)
get_foo_id = partial(crud.get_id, Foo, foo_id=None)
put_foo = partial(crud.put, Foo, foo_id=None, foo_data=None)
delete_foo = partial(crud.delete, Foo, foo_id=None)


app = connexion.App(__name__)
app.add_api('swagger.yml')


if __name__ == '__main__':
    port = os.environ.get('APP_PORT', 8080)
    DbModel.metadata.create_all(bind=engine)
    app.run(debug=True, port=int(port))

swagger.yml:

swagger: '2.0'
info:
  title: Example Foo api
  version: '0.1'
consumes:
  - application/json
produces:
  - application/json

paths:
  /foo:
    get:
      tags: [Foo]
      operationId: app.get_foo
      summary: Get all the foo
      parameters:
        - name: limit
          in: query
          type: integer
          minimum: 0
          default: 100
        - name: bar
          in: query
          type: string
          pattern: "^[a-zA-Z0-9-_]*$"
      responses:
        200:
          description: Return the foo's
          schema:
            type: array
            items:
              $ref: '#/definitions/Foo'
    post:
      tags: [Foo]
      operationId: app.post_foo
      summary: Create a new foo.
      parameters:
        - name: foo
          in: body
          schema:
            $ref: '#/definitions/Foo'
      responses:
        201:
          description: Foo created
          schema:
            $ref: '#/definitions/Foo'
        400:
          description: Failed to save foo
  /foo/{foo_id}:
    get:
      tags: [Foo]
      operationId: app.get_foo_id
      summary: Get a single foo
      parameters:
        - $ref: '#/parameters/foo_id'
      responses:
        200:
          description: Return the foo
          schema:
            $ref: '#/definitions/Foo'
        404:
          description: Foo does not exist
    put:
      tags: [Foo]
      operationId: app.put_foo
      summary: Update a foo
      parameters:
        - $ref: '#/parameters/foo_id'
        - name: foo_data
          in: body
          schema:
            $ref: '#/definitions/Foo'
      responses:
        200:
          description: Updated foo
          schema:
            $ref: '#/definitions/Foo'
        400:
          description: Failed due to invalid data
        404:
          description: Foo does not exist
    delete:
      tags: [Foo]
      operationId: app.delete_foo
      summary: Delete a foo
      parameters:
        - $ref: '#/parameters/foo_id'
      responses:
        204:
          description: Foo was deleted
        404:
          description: Foo does not exist

parameters:
  foo_id:
    name: foo_id
    description: Foo's unique identifier
    type: string
    in: path
    required: true
    pattern: "^[a-zA-Z0-9-]+$"
        
definitions:
  Foo:
    type: object
    required:
      - bar
    properties:
      id:
        type: string
        description: Unique identifier
        example: 44a288c1-829c-410d-9a6e-1fce1beb62d4
        readOnly: true
      bar:
        type: string
        description: The bar attribute
        maxLength: 40
        example: 'some bar'
      baz:
        type: number
        description: The number of baz
        minimum: 1
        example: 10
      bang:
        type: string
        description: A custom read-only variable added to every Foo
        readOnly: true