V2 API using Pecan and WSME

This change removes the v1 API completely and adds v2 using the Pecan framework
and WSME for (de)serialization.

Also add a small hack where we subclass RestController for now to support PATCH

blueprint v2-pecan

* Not added (Needs be determined)
  Invoice Lines
* Added resources
  Currency
  Language
  InvoiceStates
  PGProvider

  Merchant
    Customer
        PaymentMethods
    PaymentGatewayConfiguration
    Plan
    Product
    Subscription
    Invoice
    Usage

Change-Id: I3d252b05d5d49664e3038fa0b1fa8a6e210d03cd
This commit is contained in:
Endre Karlson 2013-07-28 18:28:59 +00:00
parent 5897c3cf5a
commit b0f909200f
42 changed files with 1208 additions and 1240 deletions

View File

@ -17,25 +17,15 @@
# under the License.
#
# Copied: Moniker
import flask
from billingstack.openstack.common import jsonutils as json
from oslo.config import cfg
API_SERVICE_OPTS = [
cfg.IntOpt('api_port', default=9091,
help='The port for the billing API server'),
cfg.IntOpt('api_listen', default='0.0.0.0', help='Bind to address'),
cfg.IntOpt('workers', default=None,
help='Number of worker processes to spawn'),
cfg.StrOpt('api_paste_config', default='api-paste.ini',
help='File name for the paste.deploy config for the api'),
cfg.StrOpt('auth_strategy', default='noauth',
help='The strategy to use for auth. Supports noauth or '
'keystone'),
]
cfg.CONF.register_opts(API_SERVICE_OPTS, 'service:api')
# Allows us to serialize datetime's etc
flask.helpers.json = json

91
billingstack/api/app.py Normal file
View File

@ -0,0 +1,91 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import pecan
from oslo.config import cfg
from wsgiref import simple_server
from billingstack import service
from billingstack.api import hooks
from billingstack.openstack.common import log
cfg.CONF.import_opt('state_path', 'billingstack.paths')
LOG = log.getLogger(__name__)
def get_config():
conf = {
'app': {
'root': 'billingstack.api.v2.controllers.root.RootController',
'modules': ['designate.api.v2'],
}
}
return pecan.configuration.conf_from_dict(conf)
def setup_app(pecan_config=None, extra_hooks=None):
app_hooks = [
hooks.NoAuthHook()
]
if extra_hooks:
app_hooks.extend(extra_hooks)
pecan_config = pecan_config or get_config()
pecan.configuration.set_config(dict(pecan_config), overwrite=True)
app = pecan.make_app(
pecan_config.app.root,
debug=cfg.CONF.debug,
hooks=app_hooks,
force_canonical=getattr(pecan_config.app, 'force_canonical', True)
)
return app
class VersionSelectorApplication(object):
def __init__(self):
self.v2 = setup_app()
def __call__(self, environ, start_response):
return self.v2(environ, start_response)
def start():
service.prepare_service()
root = VersionSelectorApplication()
host = cfg.CONF['service:api'].api_listen
port = cfg.CONF['service:api'].api_port
srv = simple_server.make_server(host, port, root)
LOG.info('Starting server in PID %s' % os.getpid())
LOG.info("Configuration:")
cfg.CONF.log_opt_values(LOG, logging.INFO)
if host == '0.0.0.0':
LOG.info('serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' %
(port, port))
else:
LOG.info("serving on http://%s:%s" % (host, port))
srv.serve_forever()

View File

@ -1,62 +0,0 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
from oslo.config import cfg
from billingstack.openstack.common import local
from billingstack.openstack.common import log as logging
from billingstack.openstack.common.context import RequestContext
from billingstack import wsgi
LOG = logging.getLogger(__name__)
def pipeline_factory(loader, global_conf, **local_conf):
"""
A paste pipeline replica that keys off of auth_strategy.
Code nabbed from cinder.
"""
pipeline = local_conf[cfg.CONF['service:api'].auth_strategy]
pipeline = pipeline.split()
filters = [loader.get_filter(n) for n in pipeline[:-1]]
app = loader.get_app(pipeline[-1])
filters.reverse()
for filter in filters:
app = filter(app)
return app
class NoAuthContextMiddleware(wsgi.Middleware):
def merchant_id(self, request):
parts = [p for p in request.path_info.split('/') if p]
if parts[0] == 'merchants' and len(parts) >= 2:
return parts[1]
def process_request(self, request):
merchant_id = self.merchant_id(request)
# NOTE(kiall): This makes the assumption that disabling authentication
# means you wish to allow full access to everyone.
context = RequestContext(is_admin=True, tenant=merchant_id)
# Store the context where oslo-log exepcts to find it.
local.store.context = context
# Attach the context to the request environment
request.environ['context'] = context

View File

@ -14,15 +14,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
import mimetypes
import pecan.rest
from flask import request, Blueprint
from wsme.types import Base, Enum, UserType, text, Unset, wsproperty
from oslo.config import cfg
from billingstack.api import utils
from billingstack.openstack.common import log
@ -43,6 +40,11 @@ CORS_ALLOW_HEADERS = [
]
class RestController(pecan.rest.RestController):
def _handle_patch(self, method, remainder):
return self._handle_post(method, remainder)
class Property(UserType):
"""
A Property that just passes the value around...
@ -57,6 +59,27 @@ class Property(UserType):
property_type = Property()
def _query_to_criterion(query, storage_func=None, **kw):
"""
Iterate over the query checking against the valid signatures (later).
:param query: A list of queries.
:param storage_func: The name of the storage function to very against.
"""
translation = {
'customer': 'customer_id'
}
criterion = {}
for q in query:
key = translation.get(q.field, q.field)
criterion[key] = q.as_dict()
criterion.update(kw)
return criterion
operation_kind = Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
@ -133,78 +156,3 @@ class ModelBase(Base):
Return a class of this object from values in the from_db
"""
return cls(**values)
class Rest(Blueprint):
"""
Helper to do stuff
"""
def get(self, rule, status_code=200, **kw):
return self._mroute('GET', rule, status_code, **kw)
def post(self, rule, status_code=202, **kw):
return self._mroute('POST', rule, status_code, **kw)
def patch(self, rule, status_code=202, **kw):
return self._mroute('PATCH', rule, status_code, **kw)
def put(self, rule, status_code=202, **kw):
return self._mroute('PUT', rule, status_code, **kw)
def delete(self, rule, status_code=204, **kw):
return self._mroute('DELETE', rule, status_code, **kw)
def _mroute(self, methods, rule, status_code=None, **kw):
if type(methods) is str:
methods = [methods]
return self.route(rule, methods=methods, status_code=status_code,
**kw)
def guess_response_type(self, type_suffix=None):
"""
Get the MIME type based on keywords / request
"""
if type_suffix:
response_type = mimetypes.guess_type("res." + type_suffix)[0]
request.response_type = response_type
def route(self, rule, sig_args=[], sig_kw={}, **options):
"""
Helper function that sets up the route as well as adding CORS..
"""
status = options.pop('status_code', None)
def decorator(func):
endpoint = options.pop('endpoint', func.__name__)
if 'body' in options and 'body' not in sig_kw:
sig_kw['body'] = options['body']
# NOTE: Wrap the function with CORS support.
@utils.crossdomain(origin=cfg.CONF.cors_allowed_origin,
max_age=cfg.CONF.cors_max_age,
headers=",".join(CORS_ALLOW_HEADERS))
@functools.wraps(func)
def handler(**kw):
# extract response content type
self.guess_response_type(kw.pop('response_type', None))
# NOTE: Extract fields (column selection)
fields = list(set(request.args.getlist('fields')))
fields.sort()
request.fields_selector = fields
if hasattr(func, '_wsme_definition'):
func._wsme_definition.status_code = status
return func(**kw)
#_rule = "/<tenant_id>" + rule
# NOTE: Add 2 set of rules, 1 with response content type and one wo
self.add_url_rule(rule, endpoint, handler, **options)
rtype_rule = rule + '.<response_type>'
self.add_url_rule(rtype_rule, endpoint, handler, **options)
return func
return decorator

40
billingstack/api/hooks.py Normal file
View File

@ -0,0 +1,40 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import hooks
from billingstack.openstack.common.context import RequestContext
class NoAuthHook(hooks.PecanHook):
"""
Simple auth - all requests will be is_admin=True
"""
def merchant_id(self, path):
"""
Get merchant id from url
"""
parts = [p for p in path.split('/') if p]
try:
index = parts.index('merchants') + 1
return parts[index]
except ValueError:
return
except IndexError:
return
def before(self, state):
merchant_id = self.merchant_id(state.request.path_url)
state.request.ctxt = RequestContext(tenant=merchant_id, is_admin=True)

View File

@ -1,80 +0,0 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
import flask
import webob.dec
from billingstack import exceptions
from billingstack import wsgi
from billingstack.openstack.common.rpc import common as rpc_common
from billingstack.openstack.common import log
from billingstack.openstack.common import jsonutils as json
LOG = log.getLogger(__name__)
class FaultWrapperMiddleware(wsgi.Middleware):
@webob.dec.wsgify
def __call__(self, request):
try:
return request.get_response(self.application)
except exceptions.Base, e:
# Handle Moniker Exceptions
status = e.error_code if hasattr(e, 'error_code') else 500
# Start building up a response
response = {
'code': status
}
if hasattr(e, 'error_type'):
response['type'] = e.error_type
if hasattr(e, 'errors'):
response['errors'] = e.errors
response['message'] = e.get_message()
return self._handle_exception(request, e, status, response)
except rpc_common.Timeout, e:
# Special case for RPC timeout's
response = {
'code': 504,
'type': 'timeout',
}
return self._handle_exception(request, e, 504, response)
except Exception, e:
# Handle all other exception types
return self._handle_exception(request, e)
def _handle_exception(self, request, e, status=500, response={}):
# Log the exception ASAP
LOG.exception(e)
headers = [
('Content-Type', 'application/json'),
]
# Set a response code, if one is missing.
if 'code' not in response:
response['code'] = status
# TODO(kiall): Send a fault notification
# Return the new response
return flask.Response(status=status, headers=headers,
response=json.dumps(response))

View File

@ -1,55 +0,0 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
from oslo.config import cfg
from paste import deploy
from billingstack.openstack.common import log as logging
from billingstack.openstack.common import wsgi
from billingstack import exceptions
from billingstack import utils
#from billingstack import policy
cfg.CONF.import_opt('state_path', 'billingstack.paths')
LOG = logging.getLogger(__name__)
class Service(wsgi.Service):
def __init__(self, backlog=128, threads=1000):
api_paste_config = cfg.CONF['service:api'].api_paste_config
config_paths = utils.find_config(api_paste_config)
if len(config_paths) == 0:
msg = 'Unable to determine appropriate api-paste-config file'
raise exceptions.ConfigurationError(msg)
LOG.info('Using api-paste-config found at: %s' % config_paths[0])
#policy.init_policy()
application = deploy.loadapp("config:%s" % config_paths[0],
name='bs_api')
super(Service, self).__init__(application=application,
host=cfg.CONF['service:api'].api_listen,
port=cfg.CONF['service:api'].api_port,
backlog=backlog,
threads=threads)

View File

@ -1,55 +0,0 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
import flask
from oslo.config import cfg
from stevedore import named
from billingstack.openstack.common import log as logging
from billingstack.api.v1.resources import bp as v1_bp
LOG = logging.getLogger(__name__)
cfg.CONF.register_opts([
cfg.ListOpt('enabled-extensions-v1', default=[],
help='Enabled API Extensions'),
], group='service:api')
def factory(global_config, **local_conf):
app = flask.Flask('billingstack.api.v1')
app.config.update(
PROPAGATE_EXCEPTIONS=True
)
app.register_blueprint(v1_bp)
# TODO(kiall): Ideally, we want to make use of the Plugin class here.
# This works for the moment though.
def _register_blueprint(ext):
app.register_blueprint(ext.plugin)
# Add any (enabled) optional extensions
extensions = cfg.CONF['service:api'].enabled_extensions_v1
if len(extensions) > 0:
extmgr = named.NamedExtensionManager('billingstack.api.v1.extensions',
names=extensions)
extmgr.map(_register_blueprint)
return app

View File

@ -1,728 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from flask import request
from flask import Response
from billingstack.api.base import Rest, Query
from billingstack.api.v1 import models
from billingstack.biller.rpcapi import biller_api
from billingstack.central.rpcapi import central_api
from billingstack.rater.rpcapi import rater_api
from wsmeext.flask import signature
bp = Rest('v1', __name__)
def _query_to_criterion(query, storage_func=None, **kw):
"""
Iterate over the query checking against the valid signatures (later).
:param query: A list of queries.
:param storage_func: The name of the storage function to very against.
"""
translation = {
'customer': 'customer_id'
}
criterion = {}
for q in query:
key = translation.get(q.field, q.field)
criterion[key] = q.as_dict()
criterion.update(kw)
return criterion
# Currencies
@bp.post('/currencies')
@signature(models.Currency, body=models.Currency)
def create_currency(body):
row = central_api.create_currency(
request.environ['context'], body.to_db())
return models.Currency.from_db(row)
@bp.get('/currencies')
@signature([models.Currency], [Query])
def list_currencies(q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_currencies(
request.environ['context'], criterion=criterion)
return map(models.Currency.from_db, rows)
@bp.get('/currencies/<currency_id>')
@signature(models.Currency, str)
def get_currency(currency_id):
row = central_api.get_currency(request.environ['context'],
currency_id)
return models.Currency.from_db(row)
@bp.put('/currencies/<currency_id>')
@signature(models.Currency, str, body=models.Currency)
def update_currency(currency_id, body):
row = central_api.update_currency(
request.environ['context'],
currency_id,
body.to_db())
return models.Currency.from_db(row)
@bp.delete('/currencies/<currency_id>')
def delete_currency(currency_id):
central_api.delete_currency(request.environ['context'], currency_id)
return Response(status=204)
# Language
@bp.post('/languages')
@signature(models.Language, body=models.Language)
def create_language(body):
row = central_api.create_language(request.environ['context'],
body.to_db())
return models.Language.from_db(row)
@bp.get('/languages')
@signature([models.Language], [Query])
def list_languages(q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_languages(
request.environ['context'], criterion=criterion)
return map(models.Language.from_db, rows)
@bp.get('/languages/<language_id>')
@signature(models.Language, str)
def get_language(language_id):
row = central_api.get_language(request.environ['context'],
language_id)
return models.Language.from_db(row)
@bp.put('/languages/<language_id>')
@signature(models.Language, str, body=models.Language)
def update_language(language_id, body):
row = central_api.update_language(
request.environ['context'],
language_id,
body.to_db())
return models.Language.from_db(row)
@bp.delete('/languages/<language_id>')
def delete_language(language_id):
central_api.delete_language(request.environ['context'], language_id)
return Response(status=204)
# PGP / PGM
@bp.get('/payment-gateway-providers')
@signature([models.PGProvider], [Query])
def list_pg_providers(q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_pg_providers(
request.environ['context'], criterion=criterion)
return map(models.PGProvider.from_db, rows)
# invoice_states
@bp.post('/invoice-states')
@signature(models.InvoiceState, body=models.InvoiceState)
def create_invoice_state(body):
row = biller_api.create_invoice_state(
request.environ['context'], body.to_db())
return models.InvoiceState.from_db(row)
@bp.get('/invoice-states')
@signature([models.InvoiceState], [Query])
def list_invoice_states(q=[]):
criterion = _query_to_criterion(q)
rows = biller_api.list_invoice_states(
request.environ['context'], criterion=criterion)
return map(models.InvoiceState.from_db, rows)
@bp.get('/invoice-states/<state_id>')
@signature(models.InvoiceState, str,)
def get_invoice_state(state_id):
row = biller_api.get_invoice_state(request.environ['context'],
state_id)
return models.InvoiceState.from_db(row)
@bp.put('/invoice-states/<state_id>')
@signature(models.InvoiceState, str, body=models.InvoiceState)
def update_invoice_state(state_id, body):
row = biller_api.update_invoice_state(
request.environ['context'],
state_id,
body.to_db())
return models.InvoiceState.from_db(row)
@bp.delete('/invoice-states/<state_id>')
def delete_invoice_state(state_id):
biller_api.delete_invoice_state(
request.environ['context'],
state_id)
return Response(status=204)
# merchants
@bp.post('/merchants')
@signature(models.Merchant, body=models.Merchant)
def create_merchant(body):
row = central_api.create_merchant(request.environ['context'],
body.to_db())
return models.Merchant.from_db(row)
@bp.get('/merchants')
@signature([models.Merchant], [Query])
def list_merchants(q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_merchants(
request.environ['context'], criterion=criterion)
return map(models.Merchant.from_db, rows)
@bp.get('/merchants/<merchant_id>')
@signature(models.Merchant, str)
def get_merchant(merchant_id):
row = central_api.get_merchant(request.environ['context'],
merchant_id)
return models.Merchant.from_db(row)
@bp.put('/merchants/<merchant_id>')
@signature(models.Merchant, str, body=models.Merchant)
def update_merchant(merchant_id, body):
row = central_api.update_merchant(
request.environ['context'],
merchant_id,
body.to_db())
return models.Merchant.from_db(row)
@bp.delete('/merchants/<merchant_id>')
def delete_merchant(merchant_id):
central_api.delete_merchant(request.environ['context'], merchant_id)
return Response(status=204)
# Invoices
@bp.post('/merchants/<merchant_id>/payment-gateways')
@signature(models.PGConfig, str, body=models.PGConfig)
def create_payment_gateway(merchant_id, body):
row = central_api.create_pg_config(
request.environ['context'],
merchant_id,
body.to_db())
return models.PGConfig.from_db(row)
@bp.get('/merchants/<merchant_id>/payment-gateways')
@signature([models.PGConfig], str, [Query])
def list_payment_gateways(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = central_api.list_pg_configs(
request.environ['context'], criterion=criterion)
return map(models.PGConfig.from_db, rows)
@bp.get('/merchants/<merchant_id>/payment-gateways/<pg_config_id>')
@signature(models.PGConfig, str, str)
def get_payment_gateway(merchant_id, pg_config_id):
row = central_api.get_pg_config(request.environ['context'], pg_config_id)
return models.PGConfig.from_db(row)
@bp.put('/merchants/<merchant_id>/payment-gateways/<pg_config_id>')
@signature(models.PGConfig, str, str, body=models.PGConfig)
def update_payment_gateway(merchant_id, pg_config_id, body):
row = central_api.update_pg_config(
request.environ['context'],
pg_config_id,
body.to_db())
return models.PGConfig.from_db(row)
@bp.delete('/merchants/<merchant_id>/payment-gateways/<pg_config_id>')
def delete_pg_config(merchant_id, pg_config_id):
central_api.delete_pg_config(
request.environ['context'],
pg_config_id)
return Response(status=204)
# customers
@bp.post('/merchants/<merchant_id>/customers')
@signature(models.Customer, str, body=models.Customer)
def create_customer(merchant_id, body):
row = central_api.create_customer(
request.environ['context'],
merchant_id,
body.to_db())
return models.Customer.from_db(row)
@bp.get('/merchants/<merchant_id>/customers')
@signature([models.Customer], str, [Query])
def list_customers(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = central_api.list_customers(
request.environ['context'], criterion=criterion)
return map(models.Customer.from_db, rows)
@bp.get('/merchants/<merchant_id>/customers/<customer_id>')
@signature(models.Customer, str, str)
def get_customer(merchant_id, customer_id):
row = central_api.get_customer(request.environ['context'],
customer_id)
return models.Customer.from_db(row)
@bp.put('/merchants/<merchant_id>/customers/<customer_id>')
@signature(models.Customer, str, str, body=models.Customer)
def update_customer(merchant_id, customer_id, body):
row = central_api.update_customer(
request.environ['context'],
customer_id,
body.to_db())
return models.Customer.from_db(row)
@bp.delete('/merchants/<merchant_id>/customers/<customer_id>')
def delete_customer(merchant_id, customer_id):
central_api.delete_customer(request.environ['context'], customer_id)
return Response(status=204)
# PaymentMethods
@bp.post('/merchants/<merchant_id>/customers/<customer_id>/payment-methods')
@signature(models.PaymentMethod, str, str, body=models.PaymentMethod)
def create_payment_method(merchant_id, customer_id, body):
row = central_api.create_payment_method(
request.environ['context'],
customer_id,
body.to_db())
return models.PaymentMethod.from_db(row)
@bp.get('/merchants/<merchant_id>/customers/<customer_id>/payment-methods')
@signature([models.PaymentMethod], str, str, [Query])
def list_payment_methods(merchant_id, customer_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id,
customer_id=customer_id)
rows = central_api.list_payment_methods(
request.environ['context'], criterion=criterion)
return map(models.PaymentMethod.from_db, rows)
@bp.get('/merchants/<merchant_id>/customers/<customer_id>/payment-methods/'
'<pm_id>')
@signature(models.PaymentMethod, str, str, str)
def get_payment_method(merchant_id, customer_id, pm_id):
row = central_api.get_payment_method(request.environ['context'], pm_id)
return models.PaymentMethod.from_db(row)
@bp.put('/merchants/<merchant_id>/customers/<customer_id>/payment-methods/'
'<pm_id>')
@signature(models.PaymentMethod, str, str, str, body=models.PaymentMethod)
def update_payment_method(merchant_id, customer_id, pm_id, body):
row = central_api.update_payment_method(request.environ['context'], pm_id,
body.to_db())
return models.PaymentMethod.from_db(row)
@bp.delete('/merchants/<merchant_id>/customers/<customer_id>/payment-methods/'
'<pm_id>')
def delete_payment_method(merchant_id, customer_id, pm_id):
central_api.delete_payment_method(request.environ['context'], pm_id)
return Response(status=204)
# Plans
@bp.post('/merchants/<merchant_id>/plans')
@signature(models.Plan, str, body=models.Plan)
def create_plan(merchant_id, body):
row = central_api.create_plan(
request.environ['context'],
merchant_id,
body.to_db())
return models.Plan.from_db(row)
@bp.get('/merchants/<merchant_id>/plans')
@signature([models.Plan], str, [Query])
def list_plans(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = central_api.list_plans(
request.environ['context'], criterion=criterion)
return map(models.Plan.from_db, rows)
@bp.get('/merchants/<merchant_id>/plans/<plan_id>')
@signature(models.Plan, str, str)
def get_plan(merchant_id, plan_id):
row = central_api.get_plan(request.environ['context'],
plan_id)
return models.Plan.from_db(row)
@bp.put('/merchants/<merchant_id>/plans/<plan_id>')
@signature(models.Plan, str, str, body=models.Plan)
def update_plan(merchant_id, plan_id, body):
row = central_api.update_plan(
request.environ['context'],
plan_id,
body.to_db())
return models.Plan.from_db(row)
@bp.delete('/merchants/<merchant_id>/plans/<plan_id>')
def delete_plan(merchant_id, plan_id):
central_api.delete_plan(request.environ['context'], plan_id)
return Response(status=204)
# Plan Item
@bp.put('/merchants/<merchant_id>/plans/<plan_id>/items/<product_id>')
@signature(models.PlanItem, str, str, str)
def add_plan_item(merchant_id, plan_id, product_id):
values = {
'plan_id': plan_id,
'product_id': product_id
}
row = central_api.create_plan_item(request.environ['context'], values)
return models.PlanItem.from_db(row)
@bp.patch('/merchants/<merchant_id>/plans/<plan_id>/items/<product_id>')
@signature(models.PlanItem, str, str, str, body=models.PlanItem)
def update_plan_item(merchant_id, plan_id, product_id, body):
row = central_api.update_plan_item(
request.environ['context'], plan_id, product_id, body.to_db())
return models.PlanItem.from_db(row)
@bp.delete('/merchants/<merchant_id>/plans/<plan_id>/items/<product_id>')
def delete_plan_item(merchant_id, plan_id, product_id):
central_api.delete_plan_item(request.environ['context'],
plan_id, product_id)
return Response(status=204)
# Products
@bp.post('/merchants/<merchant_id>/products')
@signature(models.Product, str, body=models.Product)
def create_product(merchant_id, body):
row = central_api.create_product(
request.environ['context'],
merchant_id,
body.to_db())
return models.Product.from_db(row)
@bp.get('/merchants/<merchant_id>/products')
@signature([models.Product], str, [Query])
def list_products(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = central_api.list_products(
request.environ['context'], criterion=criterion)
return map(models.Product.from_db, rows)
@bp.get('/merchants/<merchant_id>/products/<product_id>')
@signature(models.Product, str, str)
def get_product(merchant_id, product_id):
row = central_api.get_product(request.environ['context'],
product_id)
return models.Product.from_db(row)
@bp.put('/merchants/<merchant_id>/products/<product_id>')
@signature(models.Product, str, str, body=models.Product)
def update_product(merchant_id, product_id, body):
row = central_api.update_product(
request.environ['context'],
product_id,
body.to_db())
return models.Product.from_db(row)
@bp.delete('/merchants/<merchant_id>/products/<product_id>')
def delete_product(merchant_id, product_id):
central_api.delete_product(request.environ['context'], product_id)
return Response(status=204)
# Invoices
@bp.post('/merchants/<merchant_id>/invoices')
@signature(models.Invoice, str, body=models.Invoice)
def create_invoice(merchant_id, body):
row = biller_api.create_invoice(
request.environ['context'],
merchant_id,
body.to_db())
return models.Invoice.from_db(row)
@bp.get('/merchants/<merchant_id>/invoices')
@signature([models.InvoiceState], str, [Query])
def list_invoices(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = biller_api.list_invoices(
request.environ['context'], criterion=criterion)
return map(models.Invoice.from_db, rows)
@bp.get('/merchants/<merchant_id>/invoices/<invoice_id>')
@signature(models.Invoice, str, str)
def get_invoice(merchant_id, invoice_id):
row = biller_api.get_invoice(request.environ['context'],
invoice_id)
return models.Invoice.from_db(row)
@bp.put('/merchants/<merchant_id>/invoices/<invoice_id>')
@signature(models.Invoice, str, str, body=models.Invoice)
def update_invoice(merchant_id, invoice_id, body):
row = biller_api.update_invoice(
request.environ['context'],
invoice_id,
body.to_db())
return models.Invoice.from_db(row)
@bp.delete('/merchants/<merchant_id>/invoices/<invoice_id>')
def delete_invoice(merchant_id, invoice_id):
biller_api.delete_invoice(request.environ['context'], invoice_id)
return Response(status=204)
# Products
@bp.post('/merchants/<merchant_id>/invoices/<invoice_id>/lines')
@signature(models.InvoiceLine, str, str, body=models.InvoiceLine)
def create_invoice_line(merchant_id, invoice_id, body):
row = biller_api.create_invoice_line(
request.environ['context'],
invoice_id,
body.to_db())
return models.Product.from_db(row)
@bp.get('/merchants/<merchant_id>/invoices/<invoice_id>/lines')
@signature([models.InvoiceLine], str, str, [Query])
def list_invoice_lines(merchant_id, invoice_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id,
invoice_id=invoice_id)
rows = biller_api.list_invoice_lines(
request.environ['context'], criterion=criterion)
return map(models.Product.from_db, rows)
@bp.get('/merchants/<merchant_id>/invoices/<invoice_id>/lines/<line_id>')
@signature(models.InvoiceLine, str, str, str)
def get_invoice_line(merchant_id, invoice_id, line_id):
row = biller_api.get_invoice_line(request.environ['context'],
line_id)
return models.Product.from_db(row)
@bp.put('/merchants/<merchant_id>/invoices/<invoice_id>/lines/<line_id>')
@signature(models.InvoiceLine, str, str, str, body=models.InvoiceLine)
def update_invoice_line(merchant_id, invoice_id, line_id, body):
row = biller_api.update_invoice_line(
request.environ['context'],
line_id,
body.as_dict())
return models.Product.from_db(row)
@bp.delete('/merchants/<merchant_id>/invoices/<invoice_id>/lines/<line_id>')
def delete_invoice_line(merchant_id, invoice_id, line_id):
biller_api.delete_invoice_line(request.environ['context'], line_id)
return Response(status=204)
# Subscription
@bp.post('/merchants/<merchant_id>/subscriptions')
@signature(models.Subscription, str, body=models.Subscription)
def create_subscription(merchant_id, body):
row = central_api.create_subscription(
request.environ['context'],
body.to_db())
return models.Subscription.from_db(row)
@bp.get('/merchants/<merchant_id>/subscriptions')
@signature([models.Subscription], str, [Query])
def list_subscriptions(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = central_api.list_subscriptions(
request.environ['context'], criterion=criterion)
return map(models.Subscription.from_db, rows)
@bp.get('/merchants/<merchant_id>/subscriptions/<subscription_id>')
@signature(models.Subscription, str, str)
def get_subscription(merchant_id, subscription_id):
row = central_api.get_subscription(request.environ['context'],
subscription_id)
return models.Subscription.from_db(row)
@bp.put('/merchants/<merchant_id>/subscriptions/<subscription_id>')
@signature(models.Subscription, str, str, body=models.Subscription)
def update_subscription(merchant_id, subscription_id, body):
row = central_api.update_subscription(
request.environ['context'],
subscription_id,
body.to_db())
return models.Subscription.from_db(row)
@bp.delete('/merchants/<merchant_id>/subscriptions/<subscription_id>')
def delete_subscription(merchant_id, subscription_id):
central_api.delete_subscription(
request.environ['context'],
subscription_id)
return Response(status=204)
# Usage
@bp.post('/merchants/<merchant_id>/usage')
@signature(models.Usage, str, body=models.Usage)
def create_usage(merchant_id, body):
values = body.to_db()
values['merchant_id'] = merchant_id
row = rater_api.create_usage(request.environ['context'], values)
return models.Usage.from_db(row)
@bp.get('/merchants/<merchant_id>/usage')
@signature([models.Usage], str, [Query])
def list_usages(merchant_id, q=[]):
criterion = _query_to_criterion(q, merchant_id=merchant_id)
rows = rater_api.list_usages(
request.environ['context'], criterion=criterion)
return map(models.Usage.from_db, rows)
@bp.get('/merchants/<merchant_id>/usage/<usage_id>')
@signature([models.Usage], str, str)
def get_usage(merchant_id, usage_id):
row = rater_api.get_usage(request.environ['context'],
usage_id)
return models.Usage.from_db(row)
@bp.put('/merchants/<merchant_id>/usage/<usage_id>')
@signature(models.Usage, str, str, body=models.Usage)
def update_usage(merchant_id, usage_id, body):
row = rater_api.update_usage(
request.environ['context'],
usage_id,
body.to_db())
return models.Usage.from_db(row)
@bp.delete('/merchants/<merchant_id>/usage/<usage_id>')
def delete_usage(merchant_id, usage_id):
rater_api.delete_usage(
request.environ['context'],
usage_id)
return Response(status=204)

View File

@ -1,6 +1,6 @@
# Copyright 2012 Managed I.T.
# -*- encoding: utf-8 -*-
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@ -13,17 +13,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
from billingstack.openstack.common import wsgi
from oslo.config import cfg
class Middleware(wsgi.Middleware):
@classmethod
def factory(cls, global_config, **local_conf):
""" Used for paste app factories in paste.deploy config files """
def _factory(app):
return cls(app, **local_conf)
return _factory
cfg.CONF.import_opt('state_path', 'billingstack.paths')

View File

@ -1,6 +1,6 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P. All Rights Reserved.
# -*- encoding: utf-8 -*-
#
# Author: Kiall Mac Innes <kiall@hp.com>
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@ -13,21 +13,3 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
import flask
def factory(global_config, **local_conf):
app = flask.Flask('billingstack.api.versions')
@app.route('/', methods=['GET'])
def version_list():
return flask.jsonify({
"versions": [{
"id": "v1",
"status": "CURRENT"
}]
})
return app

View File

@ -0,0 +1,67 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class CurrencyController(RestController):
def __init__(self, id_):
self.id_ = id_
@wsme_pecan.wsexpose(models.Currency)
def get_all(self):
row = central_api.get_currency(request.ctxt, self.id_)
return models.Currency.from_db(row)
@wsme.validate(models.Currency)
@wsme_pecan.wsexpose(models.Currency, body=models.Currency)
def patch(self, body):
row = central_api.update_currency(request.ctxt, self.id_, body.to_db())
return models.Currency.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_currency(request.ctxt, self.id_)
class CurrenciesController(RestController):
@expose()
def _lookup(self, currency_id, *remainder):
return CurrencyController(currency_id), remainder
@wsme.validate(models.Currency)
@wsme_pecan.wsexpose(models.Currency, body=models.Currency,
status_code=202)
def post(self, body):
row = central_api.create_currency(request.ctxt, body.to_db())
return models.Currency.from_db(row)
@wsme_pecan.wsexpose([models.Currency], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_currencies(
request.ctxt, criterion=criterion)
return map(models.Currency.from_db, rows)

View File

@ -0,0 +1,74 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.api.v2.controllers.payment import PaymentMethodsController
from billingstack.central.rpcapi import central_api
class CustomerController(RestController):
payment_methods = PaymentMethodsController()
def __init__(self, id_):
self.id_ = id_
request.context['customer_id'] = id_
@wsme_pecan.wsexpose(models.Customer)
def get_all(self):
row = central_api.get_customer(request.ctxt, self.id_)
return models.Customer.from_db(row)
@wsme.validate(models.Customer)
@wsme_pecan.wsexpose(models.Customer, body=models.Customer)
def patch(self, body):
row = central_api.update_customer(request.ctxt, self.id_, body.to_db())
return models.Customer.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_customer(request.ctxt, self.id_)
class CustomersController(RestController):
@expose()
def _lookup(self, customer_id, *remainder):
return CustomerController(customer_id), remainder
@wsme.validate(models.Customer)
@wsme_pecan.wsexpose(models.Customer, body=models.Customer,
status_code=202)
def post(self, body):
row = central_api.create_customer(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Customer.from_db(row)
@wsme_pecan.wsexpose([models.Customer], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_customers(
request.ctxt, criterion=criterion)
return map(models.Customer.from_db, rows)

View File

@ -0,0 +1,73 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.biller.rpcapi import biller_api
class InvoiceController(RestController):
def __init__(self, id_):
self.id_ = id_
request.context['invoice_id'] = id_
@wsme_pecan.wsexpose(models.Invoice)
def get_all(self):
row = biller_api.get_invoice(request.ctxt, self.id_)
return models.Invoice.from_db(row)
@wsme.validate(models.Invoice)
@wsme_pecan.wsexpose(models.Invoice, body=models.Invoice)
def patch(self, body):
row = biller_api.update_invoice(request.ctxt, self.id_, body.to_db())
return models.Invoice.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
biller_api.delete_invoice(request.ctxt, self.id_)
class InvoicesController(RestController):
@expose()
def _lookup(self, invoice_id, *remainder):
return InvoiceController(invoice_id), remainder
@wsme.validate(models.Invoice)
@wsme_pecan.wsexpose(models.Invoice, body=models.Invoice, status_code=202)
def post(self, body):
row = biller_api.create_invoice(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Invoice.from_db(row)
@wsme_pecan.wsexpose([models.Invoice], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q,
merchant_id=request.context['merchant_id'])
rows = biller_api.list_invoices(
request.ctxt, criterion=criterion)
return map(models.Invoice.from_db, rows)

View File

@ -0,0 +1,68 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.biller.rpcapi import biller_api
class InvoiceStateController(RestController):
def __init__(self, id_):
self.id_ = id_
@wsme_pecan.wsexpose(models.InvoiceState)
def get_all(self):
row = biller_api.get_invoice_state(request.ctxt, self.id_)
return models.InvoiceState.from_db(row)
@wsme.validate(models.InvoiceState)
@wsme_pecan.wsexpose(models.InvoiceState, body=models.InvoiceState)
def patch(self, body):
row = biller_api.update_invoice_state(
request.ctxt, self.id_, body.to_db())
return models.InvoiceState.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
biller_api.delete_invoice_state(request.ctxt, self.id_)
class InvoiceStatesController(RestController):
@expose()
def _lookup(self, invoice_state_id, *remainder):
return InvoiceStateController(invoice_state_id), remainder
@wsme.validate(models.InvoiceState)
@wsme_pecan.wsexpose(models.InvoiceState, body=models.InvoiceState,
status_code=202)
def post(self, body):
row = biller_api.create_invoice_state(request.ctxt, body.to_db())
return models.InvoiceState.from_db(row)
@wsme_pecan.wsexpose([models.InvoiceState], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = biller_api.list_invoice_states(
request.ctxt, criterion=criterion)
return map(models.InvoiceState.from_db, rows)

View File

@ -0,0 +1,67 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class LanguageController(RestController):
def __init__(self, id_):
self.id_ = id_
@wsme_pecan.wsexpose(models.Language)
def get_all(self):
row = central_api.get_language(request.ctxt, self.id_)
return models.Language.from_db(row)
@wsme.validate(models.InvoiceState)
@wsme_pecan.wsexpose(models.Language, body=models.Language)
def patch(self, body):
row = central_api.update_language(request.ctxt, self.id_, body.to_db())
return models.Language.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_language(request.ctxt, self.id_)
class LanguagesController(RestController):
@expose()
def _lookup(self, language_id, *remainder):
return LanguageController(language_id), remainder
@wsme.validate(models.InvoiceState)
@wsme_pecan.wsexpose(models.Language, body=models.Language,
status_code=202)
def post(self, body):
row = central_api.create_language(request.ctxt, body.to_db())
return models.Language.from_db(row)
@wsme_pecan.wsexpose([models.Language], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_languages(
request.ctxt, criterion=criterion)
return map(models.Language.from_db, rows)

View File

@ -0,0 +1,85 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
from billingstack.api.v2.controllers.customer import CustomersController
from billingstack.api.v2.controllers.payment import PGConfigsController
from billingstack.api.v2.controllers.plan import PlansController
from billingstack.api.v2.controllers.product import ProductsController
from billingstack.api.v2.controllers.subscription import \
SubscriptionsController
from billingstack.api.v2.controllers.invoice import InvoicesController
from billingstack.api.v2.controllers.usage import UsagesController
class MerchantController(RestController):
customers = CustomersController()
payment_gateway_configurations = PGConfigsController()
plans = PlansController()
products = ProductsController()
subscriptions = SubscriptionsController()
invoices = InvoicesController()
usage = UsagesController()
def __init__(self, id_):
self.id_ = id_
request.context['merchant_id'] = id_
@wsme_pecan.wsexpose(models.Merchant)
def get_all(self):
row = central_api.get_merchant(request.ctxt, self.id_)
return models.Merchant.from_db(row)
@wsme.validate(models.InvoiceState)
@wsme_pecan.wsexpose(models.Merchant, body=models.Merchant)
def patch(self, body):
row = central_api.update_merchant(request.ctxt, self.id_, body.to_db())
return models.Merchant.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_merchant(request.ctxt, self.id_)
class MerchantsController(RestController):
@expose()
def _lookup(self, merchant_id, *remainder):
return MerchantController(merchant_id), remainder
@wsme.validate(models.Merchant)
@wsme_pecan.wsexpose(models.Merchant, body=models.Merchant,
status_code=202)
def post(self, body):
row = central_api.create_merchant(request.ctxt, body.to_db())
return models.Merchant.from_db(row)
@wsme_pecan.wsexpose([models.Merchant], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_merchants(
request.ctxt, criterion=criterion)
return map(models.Merchant.from_db, rows)

View File

@ -0,0 +1,139 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class PGProviders(RestController):
@wsme_pecan.wsexpose([models.PGProvider], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(q)
rows = central_api.list_pg_providers(
request.ctxt, criterion=criterion)
return map(models.PGProvider.from_db, rows)
class PGConfigController(RestController):
def __init__(self, id_):
self.id_ = id_
@wsme_pecan.wsexpose(models.PGConfig)
def get_all(self):
row = central_api.get_pg_config(request.ctxt, self.id_)
return models.PGConfig.from_db(row)
@wsme.validate(models.PGConfig)
@wsme_pecan.wsexpose(models.PGConfig, body=models.PGConfig)
def patch(self, body):
row = central_api.update_pg_config(
request.ctxt,
self.id_,
body.to_db())
return models.PGConfig.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_pg_config(request.ctxt, self.id_)
class PGConfigsController(RestController):
@expose()
def _lookup(self, method_id, *remainder):
return PGConfigController(method_id), remainder
@wsme.validate(models.PGConfig)
@wsme_pecan.wsexpose(models.PGConfig, body=models.PGConfig,
status_code=202)
def post(self, body):
row = central_api.create_pg_config(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.PGConfig.from_db(row)
@wsme_pecan.wsexpose([models.PGConfig], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q, merchant_id=request.context['merchant_id'])
rows = central_api.list_pg_configs(
request.ctxt, criterion=criterion)
return map(models.PGConfig.from_db, rows)
class PaymentMethodController(RestController):
def __init__(self, id_):
self.id_ = id_
request.context['payment_method_id'] = id_
@wsme_pecan.wsexpose(models.PaymentMethod)
def get_all(self):
row = central_api.get_payment_method(request.ctxt, self.id_)
return models.PaymentMethod.from_db(row)
@wsme.validate(models.PaymentMethod)
@wsme_pecan.wsexpose(models.PaymentMethod, body=models.PaymentMethod)
def patch(self, body):
row = central_api.update_payment_method(
request.ctxt,
self.id_,
body.to_db())
return models.PaymentMethod.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_payment_method(request.ctxt, self.id_)
class PaymentMethodsController(RestController):
@expose()
def _lookup(self, method_id, *remainder):
return PaymentMethodController(method_id), remainder
@wsme.validate(models.PaymentMethod)
@wsme_pecan.wsexpose(models.PaymentMethod, body=models.PaymentMethod,
status_code=202)
def post(self, body):
row = central_api.create_payment_method(
request.ctxt,
request.context['customer_id'],
body.to_db())
return models.PaymentMethod.from_db(row)
@wsme_pecan.wsexpose([models.PaymentMethod], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q, merchant_id=request.context['merchant_id'],
customer_id=request.context['customer_id'])
rows = central_api.list_payment_methods(
request.ctxt, criterion=criterion)
return map(models.PaymentMethod.from_db, rows)

View File

@ -0,0 +1,116 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class ItemController(RestController):
def __init__(self, id_):
self.id_ = id_
@wsme.validate(models.PlanItem)
@wsme_pecan.wsexpose(models.PlanItem, body=models.PlanItem)
def put(self, body):
values = {
'plan_id': request.context['plan_id'],
'product_id': self.id_
}
row = central_api.create_plan_item(request.ctxt, values)
return models.PlanItem.from_db(row)
@wsme.validate(models.PlanItem)
@wsme_pecan.wsexpose(models.PlanItem, body=models.PlanItem)
def patch(self, body):
row = central_api.update_plan_item(
request.ctxt,
request.context['plan_id'],
self.id_,
body.to_db())
return models.PlanItem.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self, id_):
central_api.delete_plan_item(
request.ctxt,
request.context['plan_id'],
id_)
class ItemsController(RestController):
@expose()
def _lookup(self, id_, *remainder):
return ItemController(id_), remainder
class PlanController(RestController):
items = ItemsController()
def __init__(self, id_):
self.id_ = id_
request.context['plan_id'] = id_
@wsme_pecan.wsexpose(models.Plan)
def get_all(self):
row = central_api.get_plan(request.ctxt, self.id_)
return models.Plan.from_db(row)
@wsme.validate(models.Plan)
@wsme_pecan.wsexpose(models.Plan, body=models.Plan)
def patch(self, body):
row = central_api.update_plan(request.ctxt, self.id_, body.to_db())
return models.Plan.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_plan(request.ctxt, self.id_)
class PlansController(RestController):
@expose()
def _lookup(self, plan_id, *remainder):
return PlanController(plan_id), remainder
@wsme.validate(models.Plan)
@wsme_pecan.wsexpose(models.Plan, body=models.Plan, status_code=202)
def post(self, body):
row = central_api.create_plan(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Plan.from_db(row)
@wsme_pecan.wsexpose([models.Plan], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q,
merchant_id=request.context['merchant_id'])
rows = central_api.list_plans(
request.ctxt, criterion=criterion)
return map(models.Plan.from_db, rows)

View File

@ -0,0 +1,74 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class ProductController(RestController):
def __init__(self, id_):
self.id_ = id_
request.context['product_id'] = id_
@wsme_pecan.wsexpose(models.Product)
def get_all(self):
row = central_api.get_product(request.ctxt, self.id_)
return models.Product.from_db(row)
@wsme.validate(models.Product)
@wsme_pecan.wsexpose(models.Product, body=models.Product)
def patch(self, body):
row = central_api.update_product(request.ctxt, self.id_, body.to_db())
return models.Product.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_product(request.ctxt, self.id_)
class ProductsController(RestController):
@expose()
def _lookup(self, product_id, *remainder):
return ProductController(product_id), remainder
@wsme.validate(models.Product)
@wsme_pecan.wsexpose(models.Product, body=models.Product,
status_code=202)
def post(self, body):
row = central_api.create_product(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Product.from_db(row)
@wsme_pecan.wsexpose([models.Product], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q,
merchant_id=request.context['merchant_id'])
rows = central_api.list_products(
request.ctxt, criterion=criterion)
return map(models.Product.from_db, rows)

View File

@ -0,0 +1,42 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from billingstack.openstack.common import log
from billingstack.api.v2.controllers.currency import CurrenciesController
from billingstack.api.v2.controllers.language import LanguagesController
from billingstack.api.v2.controllers.merchant import MerchantsController
from billingstack.api.v2.controllers.invoice_state import \
InvoiceStatesController
from billingstack.api.v2.controllers.payment import PGProviders
LOG = log.getLogger(__name__)
class V2Controller(object):
# Central
currencies = CurrenciesController()
languages = LanguagesController()
merchants = MerchantsController()
# Biller
invoice_states = InvoiceStatesController()
# Collector
payment_gateway_providers = PGProviders()
class RootController(object):
v2 = V2Controller()

View File

@ -0,0 +1,75 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.central.rpcapi import central_api
class SubscriptionController(RestController):
def __init__(self, id_):
self.id_ = id_
request.context['subscription_id'] = id_
@wsme_pecan.wsexpose(models.Subscription)
def get_all(self):
row = central_api.get_subscription(request.ctxt, self.id_)
return models.Subscription.from_db(row)
@wsme.validate(models.Subscription)
@wsme_pecan.wsexpose(models.Subscription, body=models.Subscription)
def patch(self, body):
row = central_api.update_subscription(request.ctxt, self.id_,
body.to_db())
return models.Subscription.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
central_api.delete_subscription(request.ctxt, self.id_)
class SubscriptionsController(RestController):
@expose()
def _lookup(self, subscription_id, *remainder):
return SubscriptionController(subscription_id), remainder
@wsme.validate(models.Subscription)
@wsme_pecan.wsexpose(models.Subscription, body=models.Subscription,
status_code=202)
def post(self, body):
row = central_api.create_subscription(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Subscription.from_db(row)
@wsme_pecan.wsexpose([models.Subscription], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q,
merchant_id=request.context['merchant_id'])
rows = central_api.list_subscriptions(
request.ctxt, criterion=criterion)
return map(models.Subscription.from_db, rows)

View File

@ -0,0 +1,73 @@
# -*- encoding: utf-8 -*-
#
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose, request
import wsme
import wsmeext.pecan as wsme_pecan
from billingstack.api.base import Query, _query_to_criterion, RestController
from billingstack.api.v2 import models
from billingstack.rater.rpcapi import rater_api
class UsageController(RestController):
def __init__(self, id_):
self.id_ = id_
request.context['usage_id'] = id_
@wsme_pecan.wsexpose(models.Usage)
def get_all(self):
row = rater_api.get_usage(request.ctxt, self.id_)
return models.Usage.from_db(row)
@wsme.validate(models.Usage)
@wsme_pecan.wsexpose(models.Usage, body=models.Usage)
def patch(self, body):
row = rater_api.update_usage(request.ctxt, self.id_, body.to_db())
return models.Usage.from_db(row)
@wsme_pecan.wsexpose(None, status_code=204)
def delete(self):
rater_api.delete_usage(request.ctxt, self.id_)
class UsagesController(RestController):
@expose()
def _lookup(self, usage_id, *remainder):
return UsageController(usage_id), remainder
@wsme.validate(models.Usage)
@wsme_pecan.wsexpose(models.Usage, body=models.Usage, status_code=202)
def post(self, body):
row = rater_api.create_usage(
request.ctxt,
request.context['merchant_id'],
body.to_db())
return models.Usage.from_db(row)
@wsme_pecan.wsexpose([models.Usage], [Query])
def get_all(self, q=[]):
criterion = _query_to_criterion(
q,
merchant_id=request.context['merchant_id'])
rows = rater_api.list_usages(
request.ctxt, criterion=criterion)
return map(models.Usage.from_db, rows)

View File

@ -16,9 +16,8 @@
"""
Base classes for API tests.
"""
from billingstack.api.v1 import factory
from billingstack.api.middleware.errors import FaultWrapperMiddleware
from billingstack.api.auth import NoAuthContextMiddleware
import pecan.testing
from billingstack.openstack.common import jsonutils as json
from billingstack.openstack.common import log
from billingstack.tests.base import ServiceTestCase
@ -44,15 +43,9 @@ class APITestMixin(object):
def make_path(self, path):
path = self._ensure_slash(path)
if self.PATH_PREFIX:
path = path + self._ensure_slash(self.PATH_PREFIX)
path = self._ensure_slash(self.PATH_PREFIX) + path
return path
def load_content(self, response):
try:
response.json = json.loads(response.data)
except ValueError:
response.json = None
def _query(self, queries):
query_params = {'q.field': [],
'q.value': [],
@ -77,17 +70,15 @@ class APITestMixin(object):
LOG.debug('GET: %s %r', path, all_params)
response = self.client.get(path,
content_type=content_type,
query_string=all_params,
headers=headers)
response = self.app.get(
path,
params=all_params,
headers=headers)
LOG.debug('GOT RESPONSE: %s', response.data)
LOG.debug('GOT RESPONSE: %s', response.body)
self.assertEqual(response.status_code, status_code)
self.load_content(response)
return response
def post(self, path, data, headers=None, content_type="application/json",
@ -97,18 +88,16 @@ class APITestMixin(object):
LOG.debug('POST: %s %s', path, data)
content = json.dumps(data)
response = self.client.post(
response = self.app.post(
path,
data=content,
content,
content_type=content_type,
headers=headers)
LOG.debug('POST RESPONSE: %r' % response.data)
LOG.debug('POST RESPONSE: %r' % response.body)
self.assertEqual(response.status_code, status_code)
self.load_content(response)
return response
def put(self, path, data, headers=None, content_type="application/json",
@ -118,17 +107,34 @@ class APITestMixin(object):
LOG.debug('PUT: %s %s', path, data)
content = json.dumps(data)
response = self.client.put(
response = self.app.put(
path,
data=content,
content,
content_type=content_type,
headers=headers)
LOG.debug('PUT RESPONSE: %r' % response.data)
LOG.debug('PUT RESPONSE: %r' % response.body)
self.assertEqual(response.status_code, status_code)
self.load_content(response)
return response
def patch_(self, path, data, headers=None, content_type="application/json",
q=[], status_code=200, **params):
path = self.make_path(path)
LOG.debug('PUT: %s %s', path, data)
content = json.dumps(data)
response = self.app.patch(
path,
content,
content_type=content_type,
headers=headers)
LOG.debug('PATCH RESPONSE: %r', response.body)
self.assertEqual(response.status_code, status_code)
return response
@ -138,9 +144,7 @@ class APITestMixin(object):
LOG.debug('DELETE: %s %r', path, all_params)
response = self.client.delete(path, query_string=all_params)
#LOG.debug('DELETE RESPONSE: %r' % response.body)
response = self.app.delete(path, params=all_params)
self.assertEqual(response.status_code, status_code)
@ -151,6 +155,7 @@ class FunctionalTest(ServiceTestCase, APITestMixin):
"""
billingstack.api base test
"""
def setUp(self):
super(FunctionalTest, self).setUp()
@ -159,7 +164,13 @@ class FunctionalTest(ServiceTestCase, APITestMixin):
self.start_service('central')
self.setSamples()
self.app = factory({})
self.app.wsgi_app = FaultWrapperMiddleware(self.app.wsgi_app)
self.app.wsgi_app = NoAuthContextMiddleware(self.app.wsgi_app)
self.client = self.app.test_client()
self.app = self.make_app()
def make_app(self):
self.config = {
'app': {
'root': 'billingstack.api.v2.controllers.root.RootController',
'modules': ['billingstack.api'],
}
}
return pecan.testing.load_test_app(self.config)

View File

@ -0,0 +1,5 @@
from billingstack.tests.api.base import FunctionalTest
class V2Test(FunctionalTest):
PATH_PREFIX = '/v2'

View File

@ -19,12 +19,12 @@ Test Currency
import logging
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
LOG = logging.getLogger(__name__)
class TestCurrency(FunctionalTest):
class TestCurrency(V2Test):
__test__ = True
path = "currencies"
@ -53,7 +53,7 @@ class TestCurrency(FunctionalTest):
_, currency = self.create_currency(fixture=1)
url = self.item_path(currency['name'])
resp = self.put(url, currency)
resp = self.patch_(url, currency)
self.assertData(resp.json, currency)

View File

@ -17,11 +17,11 @@
Test Customers.
"""
from billingstack.tests.api.base import FunctionalTest
from billingstack.api.v1.models import Customer
from billingstack.tests.api.v2 import V2Test
from billingstack.api.v2.models import Customer
class TestCustomer(FunctionalTest):
class TestCustomer(V2Test):
__test__ = True
path = "merchants/%s/customers"
@ -69,7 +69,7 @@ class TestCustomer(FunctionalTest):
expected['name'] = 'test'
url = self.item_path(self.merchant['id'], customer['id'])
resp = self.put(url, customer)
resp = self.patch_(url, customer)
self.assertData(resp.json, customer)

View File

@ -19,14 +19,14 @@ Test InvoiceState
import logging
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
LOG = logging.getLogger(__name__)
class TestInvoiceState(FunctionalTest):
class TestInvoiceState(V2Test):
__test__ = True
path = "invoice-states"
path = "invoice_states"
def setUp(self):
super(TestInvoiceState, self).setUp()
@ -59,7 +59,7 @@ class TestInvoiceState(FunctionalTest):
_, state = self.create_invoice_state()
url = self.item_path(state['name'])
resp = self.put(url, state)
resp = self.patch_(url, state)
self.assertData(resp.json, state)

View File

@ -19,12 +19,12 @@ Test Language
import logging
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
LOG = logging.getLogger(__name__)
class TestLanguage(FunctionalTest):
class TestLanguage(V2Test):
__test__ = True
path = "languages"
@ -53,7 +53,7 @@ class TestLanguage(FunctionalTest):
_, language = self.create_language(fixture=1)
url = self.item_path(language['name'])
resp = self.put(url, language)
resp = self.patch_(url, language)
self.assertData(resp.json, language)

View File

@ -17,11 +17,11 @@
Test Merchants
"""
from billingstack.tests.api.base import FunctionalTest
from billingstack.api.v1.models import Merchant
from billingstack.tests.api.v2 import V2Test
from billingstack.api.v2.models import Merchant
class TestMerchant(FunctionalTest):
class TestMerchant(V2Test):
__test__ = True
def fixture(self):
@ -51,7 +51,7 @@ class TestMerchant(FunctionalTest):
def test_update_merchant(self):
expected = Merchant.from_db(self.merchant).as_dict()
resp = self.put('merchants/' + self.merchant['id'], expected)
resp = self.patch_('merchants/' + self.merchant['id'], expected)
self.assertData(expected, resp.json)

View File

@ -19,14 +19,14 @@ Test Products
import logging
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
LOG = logging.getLogger(__name__)
class TestPaymentMethod(FunctionalTest):
class TestPaymentMethod(V2Test):
__test__ = True
path = "merchants/%s/customers/%s/payment-methods"
path = "merchants/%s/customers/%s/payment_methods"
def setUp(self):
super(TestPaymentMethod, self).setUp()
@ -82,7 +82,7 @@ class TestPaymentMethod(FunctionalTest):
self.customer['id'], method['id'])
expected = dict(fixture, name='test2')
resp = self.put(url, expected)
resp = self.patch_(url, expected)
self.assertData(expected, resp.json)
def test_delete_payment_method(self):

View File

@ -17,10 +17,10 @@
Test Plans
"""
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
class TestPlan(FunctionalTest):
class TestPlan(V2Test):
__test__ = True
path = "merchants/%s/plans"
@ -54,7 +54,7 @@ class TestPlan(FunctionalTest):
plan['name'] = 'test'
url = self.item_path(self.merchant['id'], plan['id'])
resp = self.put(url, plan)
resp = self.patch_(url, plan)
self.assertData(resp.json, plan)

View File

@ -19,12 +19,12 @@ Test Products
import logging
from billingstack.tests.api.base import FunctionalTest
from billingstack.tests.api.v2 import V2Test
LOG = logging.getLogger(__name__)
class TestProduct(FunctionalTest):
class TestProduct(V2Test):
__test__ = True
path = "merchants/%s/products"
@ -57,7 +57,7 @@ class TestProduct(FunctionalTest):
product['name'] = 'test'
url = self.item_path(self.merchant['id'], product['id'])
resp = self.put(url, product)
resp = self.patch_(url, product)
self.assertData(resp.json, product)

View File

@ -305,6 +305,17 @@ class BaseTestCase(testtools.TestCase, AssertMixin):
_values.update(values)
return _values
def path_get(self, project_file=None):
root = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..',
'..',
)
)
if project_file:
return os.path.join(root, project_file)
else:
return root
class Services(dict):
def __getattr__(self, name):

View File

@ -1,36 +0,0 @@
#!/usr/bin/env python
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copied: Moniker
import sys
import eventlet
from oslo.config import cfg
from billingstack.openstack.common import log as logging
from billingstack.openstack.common import service
from billingstack import utils
from billingstack.api import service as api_service
from billingstack.service import prepare_service
prepare_service(sys.argv)
logging.setup('wsme')
launcher = service.launch(api_service.Service(),
cfg.CONF['service:api'].workers)
launcher.wait()

View File

@ -1,36 +0,0 @@
[composite:bs_api]
use = egg:Paste#urlmap
/: bs_api_versions
/v1: bs_core_api_v1
[app:bs_api_versions]
paste.app_factory = billingstack.api.versions:factory
[composite:bs_core_api_v1]
use = call:billingstack.api.auth:pipeline_factory
noauth = noauthcontext faultwrapper bs_core_app_v1
keystone = authtoken keystonecontext faultwrapper bs_core_app_v1
[app:bs_core_app_v1]
paste.app_factory = billingstack.api.v1:factory
[filter:faultwrapper]
paste.filter_factory = billingstack.api.middleware.errors:FaultWrapperMiddleware.factory
[filter:noauthcontext]
paste.filter_factory = billingstack.api.auth:NoAuthContextMiddleware.factory
#[filter:keystonecontext]
#paste.filter_factory = billingstack.api.auth:KeystoneContextMiddleware.factory
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
service_protocol = http
service_host = 127.0.0.1
service_port = 5000
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
admin_tenant_name = %SERVICE_TENANT_NAME%
admin_user = %SERVICE_USER%
admin_password = %SERVICE_PASSWORD%

View File

@ -27,10 +27,10 @@ packages =
scripts =
bin/billingstack-db-manage
bin/billingstack-manage
bin/billingstack-api
[entry_points]
console_scripts =
billingstack-api = billingstack.api.app:start
billingstack-biller = billingstack.biller.service:launch
billingstack-central = billingstack.central.service:launch
billingstack-collector = billingstack.collector.service:launch

View File

@ -5,7 +5,7 @@ argparse
cliff>=1.4
eventlet>=0.12.0
extras
flask==0.9
pecan
iso8601>=0.1.4
netaddr
oslo.config>=1.1.0
@ -15,4 +15,4 @@ pycountry
routes>=1.12.3
stevedore>=0.9
webob>=1.2.3,<1.3
wsme>=0.5b2
https://bitbucket.org/cdevienne/wsme/get/tip.zip#egg=WSME