Bump version to 0.15~rc2-1~wheezy
[snf-cloudcms] / cloudcms / clients.py
1 """
2 CMS dynamic application clients module
3
4 Helper module to automatically retrieve client download links from different
5 sources (e.g. redmine files page).
6 """
7
8 import urllib, urllib2, cookielib, urlparse
9
10 from datetime import datetime
11 from lxml import html
12
13 from django.conf import settings
14
15 CLIENTS_CACHE_TIMEOUT = getattr(settings, 'CLOUDCMS_CLIENTS_CACHE_TIMEOUT', 120)
16
17 class VersionSource(object):
18     """
19     Base class for the different version source handlers.
20     """
21     def __init__(self, link=None, os="linux", arch="all", regex=".", name=None,
22             cache_backend=None, extra_options={}):
23         self.os = os
24         self.arch = arch
25         self.link = link
26         self.versions = []
27         self.extra_version_options = {}
28         self.extra_version_options.update(extra_options)
29         self.extra_version_options.update({'source_type': self.source_type, 'os': self.os})
30
31         self.cache_backend = cache_backend
32         self.cache_key = self.os + self.arch + self.link
33
34         if not name:
35             self.name = os
36
37         # generic urllib2 opener
38         self.opener = urllib2.build_opener(
39                     urllib2.HTTPRedirectHandler(),
40                     urllib2.HTTPHandler(debuglevel=0),
41                     urllib2.HTTPSHandler(debuglevel=0),
42                     urllib2.HTTPCookieProcessor(cookielib.CookieJar()))
43
44     def get_url(self, url):
45         """
46         Load url content and return the html etree object.
47         """
48         return html.document_fromstring(self.opener.open(url).read())
49
50     def load(self):
51         """
52         Fill self.versions attribute with dict objects of the following format
53
54         {'date': datetime.datetime(2012, 3, 16, 14, 29),
55          'link': 'http://www.domain.com/clientdownload.exe',
56          'name': 'Client download',
57          'os': 'windows',
58          'version': None}
59         """
60         raise NotImplemented
61
62     def update(self):
63         """
64         Load wrapper which handles versions caching if cache_backend is set
65         """
66         if self.cache_backend:
67             self.versions = self.cache_backend.get(self.cache_key)
68
69         if not self.versions:
70             self.load()
71
72         if self.cache_backend:
73             self.cache_backend.set(self.cache_key, self.versions, CLIENTS_CACHE_TIMEOUT)
74
75     def get_latest(self):
76         """
77         Return the latest versions
78         """
79
80         # update versions
81         self.update()
82
83         # check that at least one version is available
84         if len(self.versions):
85             version = self.versions[0]
86             version.update(self.extra_version_options)
87             return version
88
89         return None
90
91
92 class RedmineSource(VersionSource):
93     """
94     Parse a redmine project files page and return the list of existing files.
95     """
96     source_type = 'redmine_files'
97
98     def load(self):
99         """
100         Load redmine files url and extract downloads. Also parse date to be
101         able to identify latest download.
102         """
103         spliturl = urlparse.urlsplit(self.link)
104         baseurl = spliturl.geturl().replace(spliturl.path, '')
105         html = self.get_url(self.link)
106         files = html.xpath("//tr[contains(@class, 'file')]")
107
108         # helper lambdas
109         def _parse_row(row):
110             name = row.xpath("td[@class='filename']/a")[0].text
111             link = baseurl + row.xpath("td[@class='filename']/a")[0].attrib.get('href')
112             strdate = row.xpath("td[@class='created_on']")[0].text
113             date = datetime.strptime(strdate, '%m/%d/%Y %I:%M %p')
114             return {'name': name, 'link': link, 'date': date, 'version': None}
115
116         versions = map(_parse_row, files)
117         versions.sort(reverse=True, key=lambda r:r['date'])
118         self.versions = versions
119         return self
120
121
122 class DirectSource(VersionSource):
123     """
124     Direct link to a version. Dummy VersionSource which always returns one entry
125     for the provided link.
126     """
127     source_type = 'direct'
128
129     def load(self):
130         self.versions = [{'name': self.name, 'link': self.link, 'date': None}]
131         return self.versions
132
133
134 class LinkSource(DirectSource):
135     """
136     Used when version exists in some other url (e.g. apple store client)
137     """
138     source_type = 'link'
139
140
141 class ClientVersions(object):
142     """
143     Client versions manager. Given a sources dict like::
144
145     {'windows': {'source_type': 'direct', 'args':
146     ['http://clients.com/win.exe'], 'kwargs': {}},
147      'linux': {'redmine_files': 'direct',
148      'args': ['http://redmine.com/projects/client/files'],
149      'kwargs': {}}}
150
151     initializes a dict of proper VersionSource objects.
152     """
153
154     def __init__(self, sources, cache_backend=None):
155         self._sources = sources
156         self.sources = {}
157
158         for s in self._sources:
159             source_params = self._sources.get(s)
160             if source_params['type'] in SOURCE_TYPES:
161                 kwargs = {'os': s, 'cache_backend': cache_backend}
162                 args = source_params.get('args', [])
163                 self.sources[s] = SOURCE_TYPES[source_params['type']](*args, **kwargs)
164
165     def get_latest_versions(self):
166         """
167         Return the latest version of each version source.
168         """
169         for os, source in self.sources.iteritems():
170             yield source.get_latest()
171
172
173
174 class PithosXMLSource(VersionSource):
175     """
176     Extract version from versioninfo.xml
177     """
178     source_type = 'pithos_xml'
179
180     def load(self):
181         """
182         Extract first item from versioninfo.xml
183         """
184         spliturl = urlparse.urlsplit(self.link)
185         baseurl = spliturl.geturl().replace(spliturl.path, '')
186         html = self.get_url(self.link)
187         items = html.xpath("//item")
188
189         # helper lambdas
190         def _parse_row(row):
191             try:
192                 name = row.find("title").text
193                 link = row.find("enclosure").attrib["url"]
194                 strdate = row.find("pubdate").text
195                 date = datetime.strptime(strdate.split(" +")[0],
196                                          "%a, %d %B %Y %H:%M:%S")
197                 print "DATE", date
198                 version = row.find("title").text
199                 return {
200                     'name': name,
201                     'link': link,
202                     'date': date,
203                     'version': version
204                 }
205             except Exception, e:
206                 return None
207
208         versions = filter(bool, map(_parse_row, items))
209         self.versions = versions
210         return self
211
212
213 # SOURCE TYPES CLASS MAP
214 SOURCE_TYPES = {
215     'redmine_files': RedmineSource,
216     'direct': DirectSource,
217     'pithos_xml': PithosXMLSource,
218     'link': LinkSource
219 }