Framework-ul Cherrypy

Alexandru Coman
www.alexcoman.com
alex@ropython.org

Cuprins

I. Motivație
rapid, puternic, ușor de folosit

II. Dezvoltarea unei aplicații web folosind CherryPy
configurare, utilitare, dispeceri

III. CherryPy și servicii web REST


IV. Testare codului

V. Concluzii

I. Motivație


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""CherryPy - Motivație"""

import cherrypy


class Motivatie(object):

    """CherryPy oferă dezvoltatorilor posibilitatea de a construi o aplicație
    web în aceeași manieră în care ar construi orice altă aplicație folosind
    programarea obiectuală."""

    @cherrypy.expose
    @cherrypy.tools.json_out()
    def index(self):
        """Câteva avantaje aduse de CherryPy."""
        return {"avantaje": ["ușor de folosit", "foarte puternic",
                             "multe resurse", "dimensiuni mici",
                             "modificări în timp real"]} 

if __name__ == "__main__":
    # Pornim aplicațua
    cherrypy.quickstart(Motivatie()) 

II. CherryPy: Configurare

  • setările globale sunt stocate în cherrypy.config
# Putem adăuga setări folosind update
cherrypy.config.update({
        "server.socket_host": "127.0.0.1",
        "server.socket_port": 80,
        "log.error_file": "/tmp/logs/applicatia_mea/error.logs",
        "request.show_tracebacks": False,
        "environment": "production"
    })

# Sau putem încărca setările dintr-un fișier de configurare.
# Argumentul poate fi numele fișierului de configurare sau un fișier deschis
cherrypy.config.update(configurare
)
  • config.ini
[global]
server.socket_host: "0.0.0.0"
server.socket_port: 80
log.error_file: "/tmp/logs/applicatia_mea/error.logs"
request.show_tracebacks: False
environment: "production" 

II. CherryPy: Configurare

  • setările pentru aplicații se găsesc în app.conf pentru fiecare aplicație montată în parte
     
[/]
tools.trailing_slash.on = False
request.dispatch: cherrypy.dispatch.MethodDispatcher()

[/forum]
methods_with_bodies = ("POST", "PUT", "PROPPATCH")
# Putem adăuga setări pentru tot arborele de aplicații
config = {'/':{
    'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
    'tools.trailing_slash.on': False}}
cherrypy.tree.mount(Root(), config=config)

# Sau putem specifica pentru fiecare aplicație în parte setările dorite
cherrypy.tree.mount(root1, "", appconf1)
cherrypy.tree.mount(root2, "/forum", appconf2)
cherrypy.tree.mount(root3, "/blog", appconf3) 

II. CherryPy: Unelte

  • toate uneltele predefinite se găsesc în cherrypy.tools
    • cache, sesiuni, autorizare, conținut static etc

# Putem activa/dezactiva o unealtă folosind setările
[/exemplu]
tools.staticdir.on: True
tools.staticdir.root: "/path/to/app"
tools.staticdir.dir: "static" 
# Putem activa și configura uneltele pentru fiecare aplicație
class Exemplu(object):

    _cp_config = {"tools.staticdir.on": True,
                  "tools.staticdir.root": "/path/to/app",
                  "tools.staticdir.dir": "static"} 
class Exemplu(object):

    # Putem folosi unealta ca și decorator
    @tools.staticdir(root="/path/to/app", dir='static')
    def page(self):
       # ...

II. CherryPy: Unelte


O altă metodă de a defini un nou utilitar

class MentorValid(cherrypy.Tool):

    """Verificăm dacă membrul este valid"""

    def __init__(self):
        cherrypy.Tool.__init__(self, 'before_handler',
                               self.check, priority=10)

    def check(self, mentori=None):
        """Verificăm dacă parametrul mentor este în lista primită"""
        mentor = cherrypy.request.params.get('mentor', None)
        if (mentor and mentori) and mentor not in mentori:
            raise cherrypy.HTTPError(400, "Mentorul nu exista")

cherrypy.tools.mentor_valid = MentorValid() 

II. CherryPy: Unelte

Putem defini unelte care să ne adauge elemente specifice aplicației noastre sau pentru a trata anumite probleme


#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cherrypy 
def adauga_antete():
    """Adăugăm antetele recomandate pentru sporirea securității
    https://www.owasp.org/index.php/List_of_useful_HTTP_headers
    """
    headers = cherrypy.response.headers
    headers['X-Frame-Options'] = 'DENY'
    headers['X-XSS-Protection'] = '1; mode=block'
    headers['Content-Security-Policy'] = "default-src='self'" 
cherrypy.tools.adauga_antete = cherrypy.Tool('before_finalize', adauga_antete,
                                             priority=60) 

II. CherryPy: Unelte

  • dezvoltatorul poate defini noi unelte ce se vor regăsi tot în cherrypy.tools
 
import cherrypy


def protect(users):
    if cherrypy.request.login not in users:
        raise cherrypy.HTTPError("401 Unauthorized")

cherrypy.tools.protect = Tool('on_start_resource', protect)


@cherrypy.expose
@cherrypy.tools.protect(users=['alex', 'claudiu', 'cmin'])
def resource(self):
    return "Bine ai venit, {}!".format(cherrypy.request.login)
 

II. CherryPy: Unelte

Puncte de legătură:

on_start_resource primul punct de legătură
before_request_body înainte de a se procesa corpul cererii
before_handler înainte de a fi apelată o funcție ce procesează această cerere (funcție expusă)
before_finalize imediat după ce pagina a fost procesată și imediat înainte ca CherryPy să formateze răspunsul pentru client
on_end_resource procesarea s-a încheiat răspunsul este gata pentru a fi trimis spre client
before_error_response inainte de a se răspunde cu un cod de eroare
after_error_response imediat după ce s-a răspuns cu un cod de eroare
on_end_request răspunsul a fost trimis către server

II. CherryPy: Dispecer

Dispacerul predefinit
  • în momentul în care se primește o cerere se caută în componentele obiectului rădăcină cea mai bună asemănare
root = cherrypy.Application.root()
root.index = RoPython()
root.membri = Membri(["Alex", "Claudiu", "Cosmin"])
root.contact = Contact("contact@ropython.org")
root.evenimente = Evenimente(["workshop"])
root.evenimente.workshop = Workshop("Python pentru web") 
class Membri(object):
    def __init__(self, membri):
        self.membri = membri

    @cherrypy.expose
    def index(self):
        return "Echipa noastra {}".format(", ".join(self.membri))

class RoPython(object):
    membri = Membri(["Alex", "Claudiu", "Cosmin"])

    @cherrypy.expose
    def index(self):
        return "Grupul pasionaților de Python din România !" 

CherryPy: RoutesDispatcher

class Student(object):

    def __init__(self, nume):
        self.nume, self.note = nume, {}

    @cherrypy.expose
    def index(self, **kargs):
        mesaj = ["Notele elevului {} sunt".format(self.nume), str(kargs)]
        for materie in self.note:
            mesaj.append("{}: {}".format(materie, self.note[materie]))
        return "".join(mesaj)

    @cherrypy.expose
    def update(self, materie, nota):
        self.note[materie] = nota
        return "OK"
dispecer = cherrypy.dispatch.RoutesDispatcher()

dispecer.connect(name='alex', route='/alex', controller=Student('Alex'),
                 action='index', conditions=dict(method=['GET']))
dispecer.mapper.connect('alex', controller='alex',
                        action='update', conditions=dict(method=['POST']))
cherrypy.tree.mount(root=None, config={'/': {'request.dispatch': dispecer}})
cherrypy.engine.start()
cherrypy.engine.block() 

II. CherryPy: Dispecer

  • ne putem defini un dispecer propriu


import cherrypy
from cherrypy._cpdispatch import Dispatcher


class Aplicatie(object):

    @cherrypy.expose
    def salut(self, length=8):
        return "Salut !"


class URLAmuzant(Dispatcher):

    def __call__(self, path_info):
        return Dispatcher.__call__(self, path_info.lower())

if __name__ == '__main__':
    conf = {'/': {'request.dispatch': URLAmuzant()}}
    cherrypy.quickstart(Aplicatie(), '/', conf) 

II. CherryPy: Dispecer

  • metodele index, default

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cherrypy 
class Aplicatie(object):

    def index(self):
        """Prima pagina"""
        return "Bine ați venit !"

    def default(self, *args, **kargs):
        """În cazul în care nu a fost găsită nici o potrivire"""
        return "Resursa căutata nu este implementată!  "

    index.exposed = True
    despre.exposed = True
    default.exposed = True
 
cherrypy.quickstart(Aplicatie()) 

CherryPy: REST

  • folosind dispecerul predefinit și decoratorul popargs
@cherrypy.popargs('organizatie')
class Organizatie(object):

    def __init__(self):
        self.evenimente = Evenimente()

    @cherrypy.expose
    def index(self, organizatie):
        return "Bine ai venit pe: {}".format("organizatie")


@cherrypy.popargs('eveniment')
class Evenimente(object):

    @cherrypy.expose
    def index(self, organizatie, eveniment):
        return ("Despre evenimentul {} organizat de {}"
                .format(eveniment, organizatie)) 
/ropython
/ropython/evenimente/workshop1

CherryPy: REST

  • folosind dispecerul MethodDispatcher
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random
import string
import cherrypy 
class CodPin(object):
    exposed = True

    def GET(self):
        return cherrypy.session['cod_pin']

    def POST(self, lungime=4):
        cherrypy.session['cod_pin'] = ''.join(random.sample(string.hexdigits,
                                                            int(lungime)))
        return ""

    def PUT(self, cod_pin):
        cherrypy.session['cod_pin'] = cod_pin

    def DELETE(self):
        cherrypy.session.pop('cod_pin', None) 

CherryPy: REST


if __name__ == '__main__':
    conf = {
        '/': {
            'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
            'tools.sessions.on': True,
            'tools.response_headers.on': True,
            'tools.response_headers.headers': [('Content-Type', 'text/plain')],
        }
    }
    cherrypy.quickstart(CodPin(), '/', conf) 
>>> import requests
>>> sesiune = requests.Session()
>>> sesiune.post("http://127.0.0.1:8080", {"lungime": 8})
Response [200]
>>> sesiune.get("http://127.0.0.1:8080")
Response [200]
>>> sesiune.get("http://127.0.0.1:8080").text
u'8EAa94bd'
>>> sesiune.put("http://127.0.0.1:8080", {"cod_pin": "123456789"})
Response [200]
>>> sesiune.get("http://127.0.0.1:8080").text
u'123456789'
>>> sesiune.delete("http://127.0.0.1:8080")
Response [200] 

CherryPy: Rest: Exemplu


class Rezervare(object):

    exposed = True

    def __init__(self):
        self.mentor = {"Alex": [], "Claudiu": [], "Cosmin": []}

    @cherrypy.tools.json_out(content_type='application/json')
    @cherrypy.tools.mentor_valid(mentori=["Alex", "Claudiu", "Cosmin"])
    def GET(self, mentor=None):
        """Request de tip GET - Afișăm informațiile"""
        if mentor in self.mentor:
            return self.mentor[mentor]
        return self.mentor
    @cherrypy.tools.json_out(content_type='application/json')
    @cherrypy.tools.mentor_valid(mentori=["Alex", "Claudiu", "Cosmin"])
    def POST(self, mentor, echipa):
        """Request de tip POST - Adăugăm o resursă"""
        if not mentor in self.mentor:
            raise cherrypy.HTTPError(404)

        if self.mentor[mentor]:
            raise cherrypy.HTTPError(409)

        return self.mentor[mentor].append(echipa)

CherryPy: Rest: Exemplu

    @cherrypy.tools.mentor_valid(mentori=["Alex", "Claudiu", "Cosmin"])
    def DELETE(self, mentor):
        """Request de tip DELETE - Ștergem o resursă"""
        if not self.mentor[mentor]:
            raise cherrypy.HTTPError(404)
        return self.mentor[mentor].pop()


if __name__ == '__main__':
    conf = {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}}
    cherrypy.quickstart(Rezervare(), '/', conf) 
>>> requests.post("http://127.0.0.1:8080/", {"mentor": "Alex", "echipa": "RoPython"})
Response [200]
>>> requests.get("http://127.0.0.1:8080/")
Response [200]
>>> requests.get("http://127.0.0.1:8080/").text
u'{"Claudiu": [], "Alex": ["RoPython"], "Cosmin": []}'
>>> requests.delete("http://127.0.0.1:8080/Alex")
Response [200]
>>> requests.get("http://127.0.0.1:8080/")
Response [200]
>>> requests.get("http://127.0.0.1:8080/").text
u'{"Claudiu": [], "Alex": [], "Cosmin": []}'
>>> requests.delete("http://127.0.0.1:8080/Alex")
Response [404] 

IV. CherryPy: Testare

Avem aplicația de mai jos:

class Echo(object):

    """Aplicatie care va returna mesajul primit"""
    @cherrypy.expose
    def index(self):
        """Prima pagină"""
        return "Bine ați venit."

    @cherrypy.expose
    def echo(self, mesaj=None):
        """Va returna valoarea parametrului message"""
        if not mesaj:
            raise cherrypy.HTTPError(404, "Lipseste paramentrul mesaj!")
        return mesaj
 

Ca în cazul oricărei alte aplicații trebuie să luăm în calcul testarea codului.

II. CherryPy: Testare

Pentru acest lucru CherryPy ne oferă clasa "helper"
# -*- coding: utf-8 -*-
import cherrypy
from cherrypy.test import helper
class RootTest(helper.CPWebCase):

    """Suita de teste pentru aplicația Echo"""

    @staticmethod
    def setup_server():
        """Setarile pentru applicatie"""
        cherrypy.tree.mount(Echo())

    def test_mesaj_valid(self):
        """Trimitem un mesaj valid și verificăm răspunsul aplicației"""
        self.getPage("/echo?mesaj=Workshop%20RoPython%20pentru%20Web")
        self.assertStatus('200 OK')
        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
        self.assertBody('Workshop RoPython pentru Web')

    def test_lipsa_mesaj(self):
        """Nu trimitem parametrul referitor la mesaj"""
        self.getPage("/echo")
        self.assertStatus(404)

Întrebări ?


Made with Slides.com