If I have a spyne application that inherits from spyne.Application and am serving that through a spyne.WsgiApplication object, how would I add custom HTTP endpoints to the WSGI server such as / or /info?
The basic structure mirrors the one found on spyne.io
class HelloWorldService(ServiceBase):
#srpc(Unicode, Integer, _returns=Iterable(Unicode))
def say_hello(name, times):
for i in range(times):
yield 'Hello, %s' % name
application = Application([HelloWorldService], # <--- spyne.Application
tns='spyne.examples.hello',
in_protocol=Soap11(validator='lxml'),
out_protocol=JsonDocument()
)
if __name__ == '__main__':
from wsgiref.simple_server import make_server
wsgi_app = WsgiApplication(application) # <--- spyne.WsgiApplication
server = make_server('0.0.0.0', 8000, wsgi_app)
server.serve_forever()
In spyne importing from spyne.util.wsgi_wrapper import WsgiMounter (Source) will allow you to call the WsgiMounter function with a single dictionary parameter. The keys of the dictionary represent an extension of the root endpoint, and the values are the WSGI-compatible application.
For example:
def create_web_app(config):
app = Flask(__name__)
#app.route('/about')
def about():
return 'About Page'
return app
wsgi_app = WsgiMounter({
'': SpyneAppWsgi(app),
'www': create_web_app(config)
})
..will configure one server, where the spyne application will be served from the root, and everything from the create_web_app app will be served from /www. (To get to the /about page, you would route to http://localhost:8080/www/about)
In this example create_web_app returns a Flask application.
Related
I have two services that are part of one application, Hello and Auth, and each has its own target namespace, as such:
from spyne import (
Application, ServiceBase, rpc,
Unicode,
)
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication
from spyne.util.wsgi_wrapper import WsgiMounter
from wsgiref.simple_server import make_server
import logging
class Hello(ServiceBase):
#rpc(_returns=Unicode)
def hello(ctx):
return "Hello, World!"
class Auth(ServiceBase):
#rpc(_returns=Unicode)
def authenticate(ctx):
return "authenticated!"
I can see that each of these services work fine individually, like this:
hello = Application(
[Hello],
tns="hello_tns",
name="hello",
in_protocol=Soap11(validator="lxml"),
out_protocol=Soap11(),
)
auth = Application(
[Auth],
tns="auth_tns",
name="auth",
in_protocol=Soap11(validator="lxml"),
out_protocol=Soap11(),
)
hello_app = WsgiApplication(hello)
auth_app = WsgiApplication(auth)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.getLogger("spyne.protocol.xml").setLevel(logging.DEBUG)
server = make_server("0.0.0.0", 8000, hello_app)
# server = make_server("0.0.0.0", 8000, auth_app)
server.serve_forever()
I can see the wsdl in http://0.0.0.0:8000/?wsdl.
But I want to use them together as part of one application.
In this issue on GitHub and this accepted answer, it's been mentioned that I should create two instances of spyne.Application and use spyne.util.wsgi_wrapper.WsgiMounter to put them together. So I did the same:
wsgi_mounter = WsgiMounter({
"hello": hello,
"auth": auth,
})
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.getLogger("spyne.protocol.xml").setLevel(logging.DEBUG)
server = make_server("0.0.0.0", 8000, wsgi_mounter)
server.serve_forever()
But now when I look at http://0.0.0.0:8000/?wsdl, I see nothing. And the server code gives me a 404 error response:
$ python server.py
127.0.0.1 - - [09/Apr/2022 12:52:22] "GET /?wsdl HTTP/1.1" 404 0
How can I fix this?
Okay first, what a nicely written question! Thanks!
If you want both services under the same app, pass them to the same app:
hello = Application(
[Hello, Auth],
tns="hello_tns",
name="hello",
in_protocol=Soap11(validator="lxml"),
out_protocol=Soap11(),
)
When using HTTP, you can't use more than one app under the same URL.
If you want services under different URLs (which is WsgiMounter's use case) you must do like you did:
wsgi_mounter = WsgiMounter({
"hello": hello,
"auth": auth,
})
but you will find your apps under their respective URLs:
http://localhost:8000/hello/?wsdl
http://localhost:8000/auth/?wsdl
import os
from flask import Flask
from flask import request
from flask import url_for
from flask import render_template
from twilio.rest import TwilioRestClient
from twilio import twiml
Declare and configure application
app = Flask(__name__, static_url_path='/static')
ACCOUNT_SID = "AACxxxxx"
AUTH_TOKEN = "xxxxxx"
client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
Configure this number to a toll-free Twilio number to accept incoming calls.
#app.route('/caller', methods=['GET', 'POST'])
def caller():
response = twiml.Response()
response.say("Thank you for calling" \
"Please hold.")
response.enqueue("Queue Demo", waitUrl='/wait')
return str(response)
Configure waiting room to notify user of current position in the queue and
play the sweet, soothing sounds of Twilio's coffeeshop collection.
#app.route('/wait', methods=['GET', 'POST'])
def wait():
response = twiml.Response()
twilio_client.sms.messages.create(
to="+44xxxxxxxxxx",
from_="+44xxxxxxxxxx",
body="Hey Jenny! Good luck on the bar exam!",
)
response.say("You are number %s in line." % request.form['QueuePosition'])
response.play("https://s3-us-west-2.amazonaws.com/" \
"twilio/music1.mp3")
response.redirect('/wait')
return str(response)
Connect to support queue - assign to Twilio number for agent to call.
#app.route('/agent', methods=['GET', 'POST'])
def agent():
response = twiml.Response()
with response.dial() as dial:
dial.queue("Queue Demo")
return str(response)
If PORT not specified by environment, assume development config.
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
if port == 5000:
app.debug = False
app.run(host='0.0.0.0', port=port)
Why does it not send the sms?
Ok I resolved it, so if you are using python on twilio, this is the code to have your phone system that answers the call, puts the caller on hold playing music and then sends you an sms then you can call the number to help the caller.
Here it is:
import os
from flask import Flask
from flask import request
from flask import url_for
from flask import render_template
from twilio.rest import TwilioRestClient
from twilio import twiml
Declare and configure application
app = Flask(__name__, static_url_path='/static')
Configure this number to accept incoming calls.
#app.route('/caller', methods=['GET', 'POST'])
def caller():
response = twiml.Response()
response.say("Thank you for calling " \
"Please hold.")
response.enqueue("Queue Demo", waitUrl='/wait')
return str(response)
Configure the waiting room to notify the user of their current position in the queue and play the holding music or marketing message.
#app.route('/wait', methods=['GET', 'POST'])
def wait():
response = twiml.Response()
response.say("You are number %s in line." % request.form['QueuePosition'])
response.play("https://s3-us-west-2.amazonaws.com/" \
"triptorigatwilio/eventpremrigaholdmusic1.mp3")
response.redirect('/wait')
Notify agent of call via SMS
client = TwilioRestClient("your account sid...AC...", "your account auth token...xxxx")
client.sms.messages.create(
to="put number to send sms here",
from_="put the twilio number to send sms from here",
body="A caller is in the queue. Call now to help them.",
)
return str(response)
Connect to support queue - assign to Twilio number for agent to call.
#app.route('/agent', methods=['GET', 'POST'])
def agent():
response = twiml.Response()
with response.dial() as dial:
dial.queue("Queue Demo")
return str(response)
If PORT not specified by environment, assume development config.
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
if port == 5000:
app.debug = True
app.run(host='0.0.0.0', port=port)
Don't forget to configure your active numbers in twilio. The number the caller calls should point to /caller and the number for the agent calls should point to /agent. Good luck...
I'm trying to route certain URLs to a grafted WSGI app and also route sub URLs to a normal cherrypy page handler.
I need the following routes to work. All the other routes should return 404.
/api -> WSGI
/api?wsdl -> WSGI
/api/goodurl -> Page Handler
/api/badurl -> 404 Error
The WSGI app mounted at /api is a legacy SOAP based application. It needs to accept ?wsdl parameters but that is all.
I'm writing a new RESTful api at /api/some_resource.
The issue I'm having is that if the resource doesn't exist, it ends up sending the bad request to the legacy soap application. The final example "/api/badurl" ends up going to the WSGI app.
Is there a way to tell cherrypy to only send the first two routes to the WSGI app?
I wrote up a simple example of my issue:
import cherrypy
globalConf = {
'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
}
cherrypy.config.update(globalConf)
class HelloApiWsgi(object):
def __call__(self, environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return ['Hello World from WSGI']
class HelloApi(object):
#cherrypy.expose
def index(self):
return "Hello from api"
cherrypy.tree.graft(HelloApiWsgi(), '/api')
cherrypy.tree.mount(HelloApi(), '/api/hello')
cherrypy.engine.start()
cherrypy.engine.block()
Here's some unit tests:
import unittest
import requests
server = 'localhost:8080'
class TestRestApi(unittest.TestCase):
def testWsgi(self):
r = requests.get('http://%s/api?wsdl'%(server))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'Hello World from WSGI')
r = requests.get('http://%s/api'%(server))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'Hello World from WSGI')
def testGoodUrl(self):
r = requests.get('http://%s/api/hello'%(server))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'Hello from api')
def testBadUrl(self):
r = requests.get('http://%s/api/badurl'%(server))
self.assertEqual(r.status_code, 404)
Outputs:
nosetests test_rest_api.py
F..
======================================================================
FAIL: testBadUrl (webserver.test_rest_api.TestRestApi)
----------------------------------------------------------------------
Traceback (most recent call last):
File line 25, in testBadUrl
self.assertEqual(r.status_code, 404)
AssertionError: 200 != 404
-------------------- >> begin captured stdout << ---------------------
Hello World from WSGI
Preface: I can not avoid mentioning that I wish everyone would ask questions in such a complete form with means to validate the answer :-)
Solutions out of CherryPy's scope:
do URL pre-processing at front-end server, e.g. nginx
create own WSGI middleware, i.e. wrap you legacy WSGI app in another app that will filter URLs
The latter is probably the preferred way to do it, but here's CherryPy's way. Documentation section host a foreign WSGI application in CherryPy says:
You cannot use tools with a foreign WSGI application.
Also you cannot set a custom dispatcher. But you can subclass the application tree.
#!/usr/bin/env python
import cherrypy
class Tree(cherrypy._cptree.Tree):
def __call__(self, environ, start_response):
# do more complex check likewise
if environ['PATH_INFO'].startswith('/api/badurl'):
start_response('404 Not Found', [])
return []
return super(Tree, self).__call__(environ, start_response)
cherrypy.tree = Tree()
globalConf = {
'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
}
cherrypy.config.update(globalConf)
class HelloApiWsgi:
def __call__(self, environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return ['Hello World from WSGI']
class HelloApi:
#cherrypy.expose
def index(self):
return "Hello from api"
cherrypy.tree.graft(HelloApiWsgi(), '/api')
cherrypy.tree.mount(HelloApi(), '/api/hello')
if __name__ == '__main__':
cherrypy.engine.signals.subscribe()
cherrypy.engine.start()
cherrypy.engine.block()
I'm trying to set it up so that my Pyramid app logs a message whenever it does a redirect - e.g. When one of my views raises HTTPFound.
Creating a custom subclass of HTTPFound worked but it sucks to have to make sure that class is used everywhere in the app.
I then had the idea of creating a custom exception view with context=HTTPFound but that view doesn't seem to get called.
Is there a standard way to set up special handling for a redirect that is global to the app?
To log out redirects you would just have a tween that checks the return status, something like:
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPFound
def hello1(request):
raise HTTPFound(request.route_url('hello2'))
def hello2(request):
return {'boom': 'shaka'}
def redirect_logger(handler, registry):
def redirect_handler(request):
response = handler(request)
if response.status_int == 302:
print("OMGZ ITS NOT GOING WHERE YOU THINK")
return response
return redirect_handler
def main():
config = Configurator()
config.add_route('hello1', '/', view=hello1)
config.add_route('hello2', '/hi', view=hello2, renderer='json')
config.add_tween('__main__.redirect_logger')
app = config.make_wsgi_app()
return app
if __name__ == '__main__':
app = main()
server = make_server('0.0.0.0', 8080, app)
print("http://0.0.0.0:8080")
server.serve_forever()
I get this ugly "Internal Server Error" when I should be getting a neat 'page not found' page when an internal Pylons:python error occurs.
I have simulated a Python error elsewhere & the correct pagenotfound template is generated. In my config (development.ini): debug = false, full_stack = true ... so as to render this page when an exception occurs.
This is how the middleware.py looks currently:
"""Pylons middleware initialization"""
from beaker.middleware import CacheMiddleware, SessionMiddleware
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
from pylons import config
from pylons.middleware import ErrorHandler, StatusCodeRedirect
from observer import Observer
from pylons.wsgiapp import PylonsApp
from routes.middleware import RoutesMiddleware
from r4l.lib.components.middleware.striptrailingslash import StripTrailingSlash
from r4l.config.environment import load_environment
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
"""
Create a Pylons WSGI application and return it
``global_conf``
The inherited configuration for this application. Normally from
the [DEFAULT] section of the Paste ini file.
``full_stack``
Whether this application provides a full WSGI stack (by default,
meaning it handles its own exceptions and errors). Disable
full_stack when this application is "managed" by another WSGI
middleware.
``static_files``
Whether this application serves its own static files; disable
when another web server is responsible for serving them.
``app_conf``
The application's local configuration. Normally specified in
the [app:<name>] section of the Paste ini file (where <name>
defaults to main).
"""
# Configure the Pylons environment
load_environment(global_conf, app_conf)
# The Pylons WSGI app
app = PylonsApp()
# Routing/Session/Cache Middleware
app = RoutesMiddleware(app, config['routes.map'])
app = SessionMiddleware(app, config)
app = CacheMiddleware(app, config)
# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
if asbool(full_stack):
# Handle Python exceptions
app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
app = Observer(app)
# Display error documents for 401, 403, 404 status codes (and
# 500 when debug is disabled)
if asbool(config['debug']):
app = StatusCodeRedirect(app, [400, 401, 403, 404], '/content/pagenotfound')
else:
app = StatusCodeRedirect(app, [400, 401, 403, 404, 500], '/content/pagenotfound')
# Establish the Registry for this application
app = RegistryManager(app)
if asbool(static_files):
# Serve static files
static_app = StaticURLParser(config['pylons.paths']['static_files'])
app = Cascade([static_app, app])
app = StripTrailingSlash(app)
return app
Also Observer.py class
from pylons.util import call_wsgi_application
from webob import Request, Response
from weberror import formatter, collector
class Observer(object):
""" Observer object to monitor and extract the traceback for 'pagenotfound' emails """
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
status, headers, app_iter, exc_info = call_wsgi_application(
self.app, environ, catch_exc_info=True)
if exc_info is not None:
exc_data = collector.collect_exception(*exc_info)
err_report = formatter.format_text(exc_data, show_hidden_frames=True)[0]
environ['err_report'] = err_report
start_response(status, headers, exc_info)
return app_iter
this is the Error.py
import cgi
from paste.urlparser import PkgResourcesParser
from pylons import request
from pylons.controllers.util import forward
from pylons.middleware import error_document_template
from webhelpers.html.builder import literal
from r4l.lib.base import BaseController
from r4l.lib import helpers as h
class ErrorController(BaseController):
"""Generates error documents as and when they are required.
The ErrorDocuments middleware forwards to ErrorController when error
related status codes are returned from the application.
This behaviour can be altered by changing the parameters to the
ErrorDocuments middleware in your config/middleware.py file.
"""
def document(self):
"""Render the error document"""
resp = request.environ.get('pylons.original_response')
if resp.status_int == 404:
h.redirect_to('/content/pagenotfound')
content = literal(resp.body) or cgi.escape(request.GET.get('message', ''))
page = error_document_template % \
dict(prefix=request.environ.get('SCRIPT_NAME', ''),
code=cgi.escape(request.GET.get('code', str(resp.status_int))),
message=content)
return page
def img(self, id):
"""Serve Pylons' stock images"""
return self._serve_file('/'.join(['media/img', id]))
def style(self, id):
"""Serve Pylons' stock stylesheets"""
return self._serve_file('/'.join(['media/style', id]))
def _serve_file(self, path):
"""Call Paste's FileApp (a WSGI application) to serve the file
at the specified path
"""
request.environ['PATH_INFO'] = '/%s' % path
return forward(PkgResourcesParser('pylons', 'pylons'))
And finally the controller for pagenotfound func
#h.template(u'content/404')
def pagenotfound(self):
"""
Default pagenotfound page
"""
c.err_report = str(request.environ.get('pylons.original_response','')) + '\n\n'
if 'err_report' in request.environ:
c.err_report += request.environ.get('err_report','')
log.info("###### c.err_report: %s" % c.err_report)
c.page_title = u'Page Not Found'
c.previous_url = request.environ.get('HTTP_REFERER', h.url_for('/'))
pagenotfound = True
session['pagenotfound'] = pagenotfound
session.save()
if c.login:
user = Session.query(User).get(session[u'username'])
if user:
c.login.email = user.contactemail.value
This is the exception that is not being caught:
File '/home/chilton/work/cj2/CJ-7519/r4l/templates/resume/preview.html', line 441 in
${h.cj_month_year(employment.startdate)} - Current${employment.enddate.strftime('%m/%Y')}
ValueError: year=200 is before 1900; the datetime strftime() methods require year >= 1900
I've tried looking at different ways of solving this one, but without success.