eeef891a2ec86e437d2e0761356cd315cd6c9635
[restcoda.git] / rest.wsgi
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 import sys, traceback
5
6 # configuration (codes d'accès à la base MS-SQL)
7 sys.path.append('/home/thomas/public_html/')
8 import rest_config
9
10 # pour savoir quel objet interroger : Routes
11 from routes import Mapper
12 from routes.middleware import RoutesMiddleware
13 # et analyse des paramètres (POST et query_string) pour filtrage
14 from paste.request import parse_formvars
15
16 # pour chercher les données sur MS-SQL
17 from pymssql import connect
18
19 # pour afficher le résultat : jinja
20 from jinja import Environment, FileSystemLoader
21 from jinja.filters import stringfilter
22 import jinja.exceptions
23 # on prepare un environnement jinja toujours disponible
24 ejinja = Environment(loader=FileSystemLoader('/home/thomas/public_html/'))
25
26 # TODO systeme de cache : beaker
27 # from beaker.middleware import CacheMiddleware
28
29 # formats de sortie autorisés, et content-type correspondant
30 # formats = { 'xml': 'application/xml', 'html': 'text/html', 'txt': 'text/plain', 'json': 'application/json', 'rss': 'application/rss+xml' }
31 formats = { 'xml': 'application/xml', 'html': 'text/html', 'txt': 'text/plain', 'json': 'text/plain', 'rss': 'application/rss+xml' }
32
33 # les routes RESTful (cf http://routes.groovie.org/manual.html#restful-services)
34 mapper = Mapper()
35 mapper.resource('demlog','demlog')
36 mapper.resource('comlog','comlog')
37 mapper.resource('demdep','demdep')
38 mapper.resource('comdep','comdep')
39 mapper.resource('dempub','dempub')
40 mapper.resource('compub','compub')
41 mapper.resource('comare','comare')
42 mapper.resource('comarei','comare:(impl)',controller='comare') # pour les comarexxx où xxx est un code d'implantation
43 mapper.resource('comsre','comsre')
44 mapper.resource('comsrei','comsre:(impl)',controller='comsre')
45 mapper.resource('comxre','comxre') # comxre = comare + comsre
46 mapper.resource('comxrei','comxre:(impl)',controller='comxre')
47 mapper.resource('dem','dem')
48 mapper.resource('com','com')
49
50
51 class objetsql(object):
52     """objet de base : dispose d'un accès à MS-SQL (lire les données) et d'un accès à jinja (rendu des données)"""
53     def __init__(self, environ):
54         self.environ = environ
55         parse_formvars(environ)                                            # extraction des variables de query_string et POST
56         self.filters = environ['paste.parsed_formvars'][0].dict_of_lists() # puis traduction en dictionnaire {var1:[val1,val2], ...}
57     def template(self,template):
58         self.outputformat = self.environ['wsgiorg.routing_args'][1].get('format','xml')
59         return ejinja.get_template('%s.%s' % (template,self.outputformat))
60     def cursor(self):
61         if not hasattr(self,'db_connect'):
62             self.db_connect = connect(host=rest_config.host,user=rest_config.user,password=rest_config.password,database=rest_config.database)
63         if not hasattr(self,'db_cursor'):
64             self.db_cursor = self.db_connect.cursor()
65         return self.db_cursor
66     def __del__(self):
67         # coupe la connexion à MSSQL à la mort de l'objet (normalement ça se fait tout seul mais j'en suis pas sûr sûr)
68         if hasattr(self,'db_connect'):
69             db_connect.close()
70
71 class document(objetsql):
72     """objet document CODA (demlog, comlog, demdep... ils ont tous le même format)"""
73     accept_like_filters = [ 'code', 'demandeur', 'approbateur', 'code_service', 'statut', 'fournisseur' ]
74     def __init__(self, environ, code_document='%', prefix_template='document'):
75         super(document, self).__init__(environ)
76         self.code_document = code_document
77         self.prefix_template = prefix_template
78         # filtrage : on extrait la liste des filtres "acceptes" pour en faire des where
79         sqlwhere = []
80         for filter in self.accept_like_filters:
81             if filter in self.filters:
82                 or_list = []
83                 for v in self.filters[filter]:
84                     or_list.append("%s like '%s'" % (filter,v))
85                 sqlwhere.append('(' + ' or '.join(or_list) + ')')
86         # TODO : ajouter filtres min_*/max_*, avant/apres dans ce sqlwhere
87         self.sqlwhere = ''
88         for f in sqlwhere:
89             self.sqlwhere += ' and ' + f
90         # filtrage : valeur en cas de limite ("top n" en ODBC), 50 maxi
91         if 'limite' in self.filters:
92             self.sqllimit = min( int(self.filters['limite'][0]), 50 )
93         else:
94             self.sqllimit = 15
95         # TODO : ajouter filtres tri_asc/tri_desc
96
97     def _get_index(self, code):
98         """renvoie une liste de documents"""
99         # connexion a la base de données
100         cursor = self.cursor()
101         # extraction des données
102         cursor.execute("select top %d * from auf_v_acces_demcom where (code like '%s') %s order by date_modif desc" % ( self.sqllimit, code, self.sqlwhere ) )
103         items = []
104         while 1:
105             item = dict_fetchone( cursor )
106             if item == None: break
107             item['code_rest'] = coda2rest(item['code'])
108             items.append(item)
109         index = {}
110         index['code'] = code
111         index['documents'] = items
112         return index
113
114     def _get_details(self,code,id):
115         """renvoie la liste des détails pour un document"""
116         details = []
117         cursor = self.cursor()
118         cursor.execute("select * from auf_v_acces_dtls_demcom where (code like '%s') and (numero = %d)" % (code, id))
119         while 1:
120             detail = dict_fetchone(cursor)
121             if detail == None: break
122             details.append(detail)
123         return details
124
125     def _get_document(self,code,id):
126         """renvoie un document"""
127         cursor = self.cursor()
128         cursor.execute("select top 1 * from auf_v_acces_demcom where (code like '%s') and (numero = %d) %s" % (code, id, self.sqlwhere))
129         document = dict_fetchone(cursor)
130         if document == None:
131             raise "document inexistant"
132         document['code_rest'] = coda2rest( document['code'] )
133         document['details'] = self._get_details( code,id )
134         return document
135
136     def index(self):
137         """renvoie une liste de documents formatée"""
138         template = self.template('%s-index' % self.prefix_template) # préparation du modèle
139         index = self._get_index( self.code_document )               # extraction des documents concernés
140         output = template.render( index )                           # formattage via le modèle
141         return self.outputformat, output
142
143     def show(self):
144         """renvoie un document formaté"""
145         # return 'txt', '%s' % self.environ # pour debug
146         id = int(self.environ['wsgiorg.routing_args'][1]['id'])
147         template = self.template( self.prefix_template )
148         document = self._get_document( self.code_document, id )
149         output = template.render( document )
150         return self.outputformat, output
151
152 class dem(document):
153     def __init__(self, environ):
154         super(dem, self).__init__(environ, code_document = 'DEM-%')
155
156 class com(document):
157     def __init__(self, environ):
158         super(com, self).__init__(environ, code_document = 'COM-%')
159
160 class demlog(document):
161     def __init__(self, environ):
162         super(demlog, self).__init__(environ, code_document = 'DEM-LOG-AUF')
163
164 class comlog(document):
165     def __init__(self, environ):
166         super(comlog, self).__init__(environ, code_document = 'COM-LOG-AUF')
167
168 class demdep(document):
169     def __init__(self, environ):
170         super(demdep, self).__init__(environ, code_document = 'DEM-DEP-AUF')
171
172 class comdep(document):
173     def __init__(self, environ):
174         super(comdep, self).__init__(environ, code_document = 'COM-DEP-AUF')
175
176 class dempub(document):
177     def __init__(self, environ):
178         super(dempub, self).__init__(environ, code_document = 'DEM-PUB-AUF')
179
180 class compub(document):
181     def __init__(self, environ):
182         super(compub, self).__init__(environ, code_document = 'COM-PUB-AUF')
183
184 class comsre(document):
185     def __init__(self, environ):
186         # on distingue comsre et comsrexxx (avec xxx = implantation)
187         impl = environ['wsgiorg.routing_args'][1].get('impl','%')
188         super(comsre, self).__init__(environ, code_document = 'COM-SRE-%s' % impl)
189
190 class comare(document):
191     def __init__(self, environ):
192         impl = environ['wsgiorg.routing_args'][1].get('impl','%')
193         super(comare, self).__init__(environ, code_document = 'COM-ARE-%s' % impl)
194
195 class comxre(document):
196     def __init__(self, environ):
197         impl = environ['wsgiorg.routing_args'][1].get('impl','%')
198         super(comxre, self).__init__(environ, code_document = 'COM-%%RE-%s' % impl)
199
200 #
201 # fin des objets accessibles
202 #
203
204 def dispatcher(environ, start_response):
205     """dispatch vers la bonne methode du bon objet, et retour WSGI"""
206     results = environ['wsgiorg.routing_args'][1]
207     try:
208         target_class = globals()[results['controller']]
209         method = getattr(target_class,results['action'])
210     except:
211         start_response("404 Not Found", [('Content-type', 'text/html')])
212         return '<html><body><h2>404 objet ou action invalide</h2><pre>%s: %s\n%s</pre></body></html>' % ( sys.exc_info()[0] , sys.exc_info()[1] , traceback.format_exc())
213     try:
214         type, output = method(target_class(environ))
215         start_response("200 OK", [('Content-type', formats[type])])
216         return output.encode('utf-8')
217     except jinja.exceptions.TemplateNotFound, template:
218         start_response("415 Unsupported Media Type", [('Content-type', 'text/html')])
219         return '<html><body><h2>415 format non supporté (%s inexistant)</h2></body></html>' % template
220     except:
221         start_response("500 INTERNAL ERROR", [('Content-type', 'text/html')])
222         return '<html><body><h2>500 erreur lors du traitement</h2><pre>%s: %s\n%s</pre></body></html>' % ( sys.exc_info()[0] , sys.exc_info()[1] , traceback.format_exc())
223
224 # application qui sera lancée par mod_wsgi : on route et on dispatche
225 application = RoutesMiddleware( dispatcher, mapper)
226 # TODO : ajouter un middleware de cache (beaker, basé sur REQUEST_URI quand la methode est GET)
227
228 #
229 # petits utilitaires
230 #
231 def dict_fetchone(cursor):
232     """Renvoie le resultat d'un fetchone dans un dictionnaire"""
233     result = cursor.fetchone()
234     if result == None: return None
235     result_dict = {}
236     for i in range(len(result)):
237         if isinstance( result[i], str ):
238             result_dict[cursor.description[i][0]] = result[i].decode('iso-8859-1')
239         else:
240             result_dict[cursor.description[i][0]] = result[i]
241     return result_dict
242
243 import re 
244 p = re.compile('(dem|com)-(...)-(...)',re.IGNORECASE)
245 def coda2rest(value):
246     """Traduit un nom CODA vers la base REST correspodante, 
247     par exemple DEM-LOG-AUF en demlog ou COM-ARE-VN3 en comarevn3"""
248     m = p.search(value)
249     if m == None: return value
250     if m.group(3).lower() == 'auf':
251         return m.group(1).lower() + m.group(2).lower()
252     else:
253         return m.group(1).lower() + m.group(2).lower() + m.group(3).lower()
254