Commit ac7f9f36 authored by Romain Bignon's avatar Romain Bignon

Update of modules

parent ffe0cb16
......@@ -29,6 +29,7 @@ from weboob.browser.elements import DictElement, ItemElement, ListElement, metho
from weboob.browser.filters.standard import Date, Env, CleanText, Field, ItemNotFound, BrowserURL
from weboob.browser.filters.json import Dict
from weboob.tools.date import parse_french_date
from weboob.tools.compat import basestring
class ArteItemElement(ItemElement):
......
......@@ -17,6 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
import itertools
from weboob.tools.test import BackendTest
from weboob.tools.value import Value
......@@ -45,11 +46,11 @@ class ArteTest(BackendTest):
def test_sites(self):
for site in SITE.values:
l1 = list(self.backend.iter_resources([BaseVideo], [site.get('id')]))
l1 = list(itertools.islice(self.backend.iter_resources([BaseVideo], [site.get('id')]), 0, 20))
assert len(l1)
while not isinstance(l1[0], BaseVideo):
l1 = list(self.backend.iter_resources([BaseVideo], l1[-1].split_path))
l1 = list(itertools.islice(self.backend.iter_resources([BaseVideo], l1[-1].split_path), 0, 20))
assert len(l1)
for v in l1:
......
......@@ -154,9 +154,9 @@ class AuMBrowser(DomainBrowser):
headers = kwargs.setdefault('headers', {})
if 'applications' not in url:
today = local2utc(datetime.now()).strftime('%Y-%m-%d')
token = sha256(self.username + self.APITOKEN + today).hexdigest()
token = sha256((self.username + self.APITOKEN + today).encode('utf-8')).hexdigest()
headers['Authorization'] = 'Basic %s' % (b64encode('%s:%s' % (self.username, self.password)))
headers['Authorization'] = 'Basic %s' % (b64encode(b'%s:%s' % (self.username.encode('utf-8'), self.password.encode('utf-8')))).decode('utf-8')
headers['X-Platform'] = 'android'
headers['X-Client-Version'] = self.APIVERSION
headers['X-AUM-Token'] = token
......@@ -181,8 +181,8 @@ class AuMBrowser(DomainBrowser):
return self.consts
self.consts = [{}, {}]
for key, sexes in self.request('values').iteritems():
for sex, values in sexes.iteritems():
for key, sexes in self.request('values').items():
for sex, values in sexes.items():
if sex in ('boy', 'both'):
self.consts[0][key] = values
if sex in ('girl', 'both'):
......
......@@ -26,6 +26,7 @@ from collections import OrderedDict
from weboob.capabilities.contact import Contact as _Contact, ProfileNode
from weboob.tools.html import html2text
from weboob.tools.compat import unicode, basestring
class FieldBase(object):
......@@ -242,7 +243,7 @@ class Contact(_Contact):
self.profile = OrderedDict()
if 'sex' in profile:
for section, d in self.TABLE.iteritems():
for section, d in self.TABLE.items():
flags = ProfileNode.SECTION
if section.startswith('_'):
flags |= ProfileNode.HEAD
......@@ -254,7 +255,7 @@ class Contact(_Contact):
s = ProfileNode(section, section.capitalize(), OrderedDict(), flags=flags)
for key, builder in d.iteritems():
for key, builder in d.items():
try:
value = builder.get_value(profile, consts[int(profile['sex'])])
except KeyError:
......
......@@ -36,6 +36,7 @@ from weboob.exceptions import BrowserUnavailable, BrowserHTTPNotFound
from weboob.tools.value import Value, ValueBool, ValueBackendPassword
from weboob.tools.date import local2utc
from weboob.tools.misc import to_unicode
from weboob.tools.compat import unicode, long, basestring
from .contact import Contact
from .antispam import AntiSpam
......@@ -104,7 +105,7 @@ class AuMModule(Module, CapMessages, CapMessagesPost, CapDating, CapChat, CapCon
all_events[u'baskets'] = (self.browser.get_baskets, 'You were put into %s\'s basket')
all_events[u'flashs'] = (self.browser.get_flashs, 'You sent a charm to %s')
all_events[u'visits'] = (self.browser.get_visits, 'Visited by %s')
for type, (events, message) in all_events.iteritems():
for type, (events, message) in all_events.items():
for event in events():
e = Event(event['who']['id'])
......@@ -343,7 +344,7 @@ class AuMModule(Module, CapMessages, CapMessagesPost, CapDating, CapChat, CapCon
if 'profile' in fields:
contact = self.get_contact(contact)
if contact and 'photos' in fields:
for name, photo in contact.photos.iteritems():
for name, photo in contact.photos.items():
if photo.url and not photo.data:
data = self.browser.openurl(photo.url).read()
contact.set_photo(name, data=data)
......
......@@ -22,7 +22,7 @@ from __future__ import unicode_literals
from weboob.browser import LoginBrowser, URL, need_login
from weboob.capabilities.base import find_object
from .pages import LoginPage, LoansPage, RenewPage
from .pages import LoginPage, LoansPage, RenewPage, SearchPage
class BibliothequesparisBrowser(LoginBrowser):
......@@ -31,6 +31,7 @@ class BibliothequesparisBrowser(LoginBrowser):
login = URL(r'/Default/Portal/Recherche/logon.svc/logon', LoginPage)
bookings = URL('/Default/Portal/Recherche/Search.svc/RenderAccountWebFrame', LoansPage)
renew = URL(r'/Default/Portal/Services/ILSClient.svc/RenewLoans', RenewPage)
search = URL(r'/Default/Portal/Recherche/Search.svc/Search', SearchPage)
json_headers = {
'Accept': 'application/json, text/javascript',
......@@ -59,3 +60,26 @@ class BibliothequesparisBrowser(LoginBrowser):
assert b._renew_data, 'book has no data'
post = u'{"loans":[%s]}' % b._renew_data
self.renew.go(data=post.encode('utf-8'), headers=self.json_headers)
def search_books(self, pattern):
max_page = 0
page = 0
while page <= max_page:
d = {
"query": {
"Page": page,
"PageRange": 3,
"QueryString": pattern,
"ResultSize": 50,
"ScenarioCode": "CATALOGUE",
"SearchContext": 0,
"SearchLabel": "",
"Url": "https://bibliotheques.paris.fr/Default/search.aspx?SC=CATALOGUE&QUERY={q}&QUERY_LABEL=#/Search/(query:(Page:{page},PageRange:3,QueryString:{q},ResultSize:50,ScenarioCode:CATALOGUE,SearchContext:0,SearchLabel:''))".format(q=pattern, page=page),
}
}
self.location('/Default/Portal/Recherche/Search.svc/Search', json=d, headers=self.json_headers)
for book in self.page.iter_books():
yield book
max_page = self.page.get_max_page()
page += 1
......@@ -62,5 +62,5 @@ class BibliothequesparisModule(Module, CapBook):
def renew_book(self, _id):
return self.browser.do_renew(_id)
def search_books(self, _string):
raise NotImplementedError()
def search_books(self, pattern):
return self.browser.search_books(pattern)
......@@ -20,9 +20,10 @@
from __future__ import unicode_literals
from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage
from weboob.browser.elements import ListElement, ItemElement, method
from weboob.browser.elements import ListElement, ItemElement, method, DictElement
from weboob.browser.filters.standard import CleanText, Date, Regexp, Field
from weboob.browser.filters.html import Link
from weboob.browser.filters.json import Dict
from weboob.capabilities.base import UserError
from weboob.capabilities.library import Book
......@@ -82,3 +83,20 @@ class LoansPage(LoggedPage, JsonMixin):
class RenewPage(LoggedPage, JsonMixin):
pass
class SearchPage(LoggedPage, JsonPage):
@method
class iter_books(DictElement):
item_xpath = 'd/Results'
class item(ItemElement):
klass = Book
obj_url = Dict('FriendlyUrl')
obj_id = Dict('Resource/RscId')
obj_name = Dict('Resource/Ttl')
obj_author = Dict('Resource/Crtr', default=None)
def get_max_page(self):
return self.doc['d']['SearchInfo']['PageMax']
......@@ -17,15 +17,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import datetime
import re
import os
from dateutil.parser import parse as parse_date
from weboob.capabilities.base import empty
from weboob.browser.browsers import APIBrowser
from weboob.browser.cache import CacheMixin
from weboob.browser.exceptions import ClientError
from weboob.tools.compat import quote_plus
__all__ = ['GithubBrowser']
......@@ -46,6 +47,11 @@ class GithubBrowser(CacheMixin, APIBrowser):
'id': project_id
}
def iter_labels(self, project_id):
json = self.request('https://api.github.com/repos/%s/labels' % project_id)
for d in json:
yield d['name']
def get_issue(self, project_id, issue_number):
json = self.request('https://api.github.com/repos/%s/issues/%s' % (project_id, issue_number))
return self._make_issue(project_id, issue_number, json)
......@@ -60,7 +66,12 @@ class GithubBrowser(CacheMixin, APIBrowser):
break
def iter_issues(self, query):
qsparts = ['repo:%s' % query.project]
def escape(s):
if ' ' in s:
return '"%s"' % s
return s
qsparts = ['repo:%s' % query.project.id]
if query.assignee:
qsparts.append('assignee:%s' % query.assignee)
if query.author:
......@@ -69,14 +80,16 @@ class GithubBrowser(CacheMixin, APIBrowser):
qsparts.append('state:%s' % query.status)
if query.title:
qsparts.append('%s in:title' % query.title)
if query.tags:
qsparts.append(' '.join('label:%s' % escape(tag) for tag in query.tags))
qs = quote_plus(' '.join(qsparts))
qs = ' '.join(qsparts)
base_url = 'https://api.github.com/search/issues?q=%s' % qs
for json in self._paginated(base_url):
base_url = 'https://api.github.com/search/issues'
for json in self._paginated(base_url, params={'q': qs}):
for jissue in json['items']:
issue_number = jissue['number']
yield self._make_issue(query.project, issue_number, jissue)
yield self._make_issue(query.project.id, issue_number, jissue)
if not len(json['items']):
break
......@@ -105,6 +118,8 @@ class GithubBrowser(CacheMixin, APIBrowser):
data['milestone'] = issue.version.id
if issue.status:
data['state'] = issue.status.name # TODO improve if more statuses are implemented
if not empty(issue.tags):
data['labels'] = [tag.name for tag in issue.tags]
return data
def post_comment(self, issue_id, comment):
......@@ -135,6 +150,7 @@ class GithubBrowser(CacheMixin, APIBrowser):
d['version'] = None
d['has_comments'] = (json['comments'] > 0)
d['attachments'] = list(self._extract_attachments(d['body']))
d['labels'] = json['labels']
# TODO fetch other updates?
return d
......@@ -161,6 +177,48 @@ class GithubBrowser(CacheMixin, APIBrowser):
if len(json) < 100:
break
EVENTS = {
'closed': ('state', 'open', 'closed'),
'merged': ('state', 'open', 'closed'),
'reopened': ('state', 'closed', 'open'),
'assigned': ('assignee', None, lambda j: j['assignee']['login']),
'unassigned': ('assignee', lambda j: j['assignee']['login'], None),
'labeled': ('tags', None, lambda j: j['label']['name']),
'unlabeled': ('tags', lambda j: j['label']['name'], None),
'renamed': ('title', lambda j: j['rename']['from'], lambda j: j['rename']['to']),
'locked': ('locked', 'unlocked', 'locked'),
'unlocked': ('locked', 'locked', 'unlocked'),
'milestoned': ('milestone', None, lambda j: j['milestone']['title']),
'demilestoned': ('milestone', lambda j: j['milestone']['title'], None),
'marked_as_duplicate': ('duplicate', 'no', 'yes'), # no link to other issue?
'unmarked_as_duplicate': ('duplicate', 'yes', 'no'),
}
def iter_events(self, project_id, issue_number):
url = 'https://api.github.com/repos/%s/issues/%s/events' % (project_id, issue_number)
for json in self._paginated(url):
for jevent in json:
d = {}
d['id'] = jevent['id']
d['author'] = jevent['actor']['login']
d['date'] = parse_date(jevent['created_at'])
if jevent['event'] not in self.EVENTS:
self.logger.info('ignoring event %r', jevent['event'])
continue
d['field'], old, new = self.EVENTS[jevent['event']]
if callable(old):
old = old(jevent)
if callable(new):
new = new(jevent)
d['old'] = old
d['new'] = new
yield d
if len(json) < 100:
break
def _extract_attachments(self, message):
for attach_url in re.findall(r'https://f.cloud.github.com/assets/[\w/.-]+', message):
yield {
......@@ -168,13 +226,13 @@ class GithubBrowser(CacheMixin, APIBrowser):
'filename': os.path.basename(attach_url)
}
def _paginated(self, url, start_at=1):
def _paginated(self, url, start_at=1, params=None):
params = (params or {}).copy()
params['per_page'] = 100
while True:
if '?' in url:
page_url = '%s&per_page=100&page=%s' % (url, start_at)
else:
page_url = '%s?per_page=100&page=%s' % (url, start_at)
yield self.request(page_url)
params['page'] = start_at
yield self.request(url, params=params)
start_at += 1
def get_user(self, _id):
......@@ -233,11 +291,4 @@ class GithubBrowser(CacheMixin, APIBrowser):
else:
return {}
# TODO use a cache for objects and/or pages?
def parse_date(s):
if s.endswith('Z'):
s = s[:-1]
return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S')
# TODO use a cache for objects?
......@@ -20,7 +20,11 @@
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import Value, ValueBackendPassword
from weboob.capabilities.bugtracker import CapBugTracker, Issue, Project, User, Version, Status, Update, Attachment
from weboob.capabilities.base import empty
from weboob.capabilities.bugtracker import (
CapBugTracker, Issue, Project, User, Version, Status, Update, Attachment,
Change,
)
from .browser import GithubBrowser
......@@ -30,7 +34,6 @@ __all__ = ['GithubModule']
STATUSES = {'open': Status('open', u'open', Status.VALUE_NEW),
'closed': Status('closed', u'closed', Status.VALUE_RESOLVED)}
# TODO tentatively parse github "labels"?
class GithubModule(Module, CapBugTracker):
......@@ -41,7 +44,7 @@ class GithubModule(Module, CapBugTracker):
LICENSE = 'AGPLv3+'
VERSION = '1.4'
CONFIG = BackendConfig(Value('username', label='Username', default=''),
ValueBackendPassword('password', label='Password', default=''))
ValueBackendPassword('password', label='Password or Personal token', default=''))
BROWSER = GithubBrowser
......@@ -59,8 +62,10 @@ class GithubModule(Module, CapBugTracker):
project = Project(_id, d['name'])
project.members = list(self._iter_members(project.id))
project.statuses = list(STATUSES.values())
project.fields = [] # not supported by github
project.categories = []
project.versions = list(self._iter_versions(project.id))
project.tags = list(self.browser.iter_labels(project.id))
return project
......@@ -73,19 +78,32 @@ class GithubModule(Module, CapBugTracker):
issue = self._make_issue(d, project)
if d['has_comments']:
self._fetch_comments(issue)
self._fetch_events(issue)
issue.history.sort(key=lambda u: u.date)
return issue
def iter_issues(self, query):
if ((query.assignee, query.author, query.status, query.title) ==
(None, None, None, None)):
it = self.browser.iter_project_issues(query.project)
if not query.project:
return
query = query.copy()
if query.project and not isinstance(query.project, Project):
query.project = self.get_project(query.project)
if isinstance(query.status, Status):
query.status = query.status.name
if isinstance(query.author, User):
query.author = query.author.name
if isinstance(query.assignee, User):
query.assignee = query.assignee.name
if empty(query.assignee) and empty(query.author) and empty(query.status) and empty(query.title) and empty(query.tags):
it = self.browser.iter_project_issues(query.project.id)
else:
it = self.browser.iter_issues(query)
project = self.get_project(query.project)
for d in it:
issue = self._make_issue(d, project)
issue = self._make_issue(d, query.project)
yield issue
def create_issue(self, project_id):
......@@ -151,6 +169,8 @@ class GithubModule(Module, CapBugTracker):
issue.attachments = [self._make_attachment(dattach) for dattach in d['attachments']]
issue.tags = [t['name'] for t in d['labels']]
return issue
def _fetch_comments(self, issue):
......@@ -159,6 +179,12 @@ class GithubModule(Module, CapBugTracker):
issue.history = []
issue.history += [self._make_comment(dcomment, issue.project) for dcomment in self.browser.iter_comments(project_id, issue_number)]
def _fetch_events(self, issue):
project_id, issue_number = self._extract_issue_id(issue.id)
if not issue.history:
issue.history = []
issue.history += [self._make_update(dcomment, issue.project) for dcomment in self.browser.iter_events(project_id, issue_number)]
def _make_attachment(self, d):
a = Attachment(d['url'])
a.url = d['url']
......@@ -177,6 +203,22 @@ class GithubModule(Module, CapBugTracker):
u.attachments = [self._make_attachment(dattach) for dattach in d['attachments']]
return u
def _make_update(self, d, project):
u = Update(d['id'])
u.author = project.find_user(d['author'], None)
if not u.author:
# may duplicate users
u.author = User(d['author'], d['author'])
u.date = d['date']
c = Change()
c.field = d['field']
c.last = d['old']
c.new = d['new']
u.changes = [c]
return u
@staticmethod
def _extract_issue_id(_id):
return _id.rsplit('/', 1)
......@@ -184,3 +226,15 @@ class GithubModule(Module, CapBugTracker):
@staticmethod
def _build_issue_id(project_id, issue_number):
return '%s/%s' % (project_id, issue_number)
def fill_issue(self, issue, fields):
if set(['history']) & set(fields):
new = self.get_issue(issue.id)
for f in fields:
if empty(getattr(issue, f)):
setattr(issue, f, getattr(new, f))
OBJECTS = {
Issue: fill_issue,
}
......@@ -22,7 +22,7 @@ from __future__ import unicode_literals
from collections import OrderedDict
from weboob.tools.backend import Module, BackendConfig
from weboob.capabilities.base import StringField, UserError
from weboob.capabilities.base import UserError
from weboob.capabilities.gauge import CapGauge, GaugeSensor, Gauge, GaugeMeasure, SensorNotFound
from weboob.tools.value import Value, ValueBackendPassword
......@@ -36,7 +36,7 @@ SENSOR_TYPES = OrderedDict([('available_bikes', 'Available bikes'),
('available_bike_stands', 'Free stands'),
('bike_stands', 'Total stands')])
CITIES = ("Paris", "Rouen", "Toulouse", "Luxembourg", "Valence", "Stockholm",
CITIES = ("Rouen", "Toulouse", "Luxembourg", "Valence", "Stockholm",
"Goteborg", "Santander", "Amiens", "Lillestrom", "Mulhouse", "Lyon",
"Ljubljana", "Seville", "Namur", "Nancy", "Creteil", "Bruxelles-Capitale",
"Cergy-Pontoise", "Vilnius", "Toyama", "Kazan", "Marseille", "Nantes",
......@@ -48,11 +48,6 @@ class BikeMeasure(GaugeMeasure):
return '<GaugeMeasure level=%d>' % self.level
class BikeSensor(GaugeSensor):
longitude = StringField('Longitude of the sensor')
latitude = StringField('Latitude of the sensor')
class jcvelauxModule(Module, CapGauge):
NAME = 'jcvelaux'
DESCRIPTION = ('City bike renting availability information.\nCities: %s' %
......@@ -64,7 +59,7 @@ class jcvelauxModule(Module, CapGauge):
BROWSER = VelibBrowser
CONFIG = BackendConfig(Value('city', label='City', default='Paris',
CONFIG = BackendConfig(Value('city', label='City', default='Lyon',
choices=CITIES + ("ALL",)),
ValueBackendPassword('api_key', label='Optional API key',
default='', noprompt=True))
......@@ -86,7 +81,7 @@ class jcvelauxModule(Module, CapGauge):
def _make_sensor(self, sensor_type, info, gauge):
id = '%s.%s' % (sensor_type, gauge.id)
sensor = BikeSensor(id)
sensor = GaugeSensor(id)
sensor.gaugeid = gauge.id
sensor.name = SENSOR_TYPES[sensor_type]
sensor.address = '%s' % info['address']
......
......@@ -24,32 +24,8 @@ from datetime import date, time, datetime, timedelta
from weboob.browser.elements import method, ListElement, ItemElement
from weboob.browser.filters.standard import CleanText, Field
from weboob.browser.pages import HTMLPage
from weboob.capabilities.base import FloatField, IntField, Field as BaseField
from weboob.capabilities.weather import City, Forecast, Temperature, Current
from weboob.tools.compat import quote, unicode
class DIRECTION(object):
S = 'South'
N = 'North'
E = 'East'
W = 'West'
SE = 'South-East'
SW = 'South-West'
NW = 'North-West'
NE = 'North-East'
class FullForecast(Forecast):
wind_speed = IntField('Wind speed (in m/s)')
wind_direction = BaseField('Wind direction', unicode)
humidity = FloatField('Relative humidity ratio')
class FullCurrent(Current):
wind_speed = IntField('Wind speed (in m/s)')
wind_direction = BaseField('Wind direction', unicode)
humidity = FloatField('Relative humidity ratio')
from weboob.capabilities.weather import City, Forecast, Temperature, Current, Direction
from weboob.tools.compat import quote
class CitiesPage(HTMLPage):
......@@ -65,7 +41,7 @@ class CitiesPage(HTMLPage):
obj_name = CleanText('.')
def obj_id(self):
return quote(Field('name')(self).encode('utf-8'))
return quote(Field('name')(self))
def temp(v):
......@@ -85,8 +61,8 @@ class WeatherPage(HTMLPage):
humidity = self.get_cell(self.titles['Humidité relative'], n)
obj.humidity = float(humidity.strip('%')) / 100
direction = self.get_cell(self.titles['Direction du vent'], n)[-2:].replace('O', 'W')
obj.wind_direction = getattr(DIRECTION, direction)
direction = self.get_cell(self.titles['Direction du vent'], n).replace('O', 'W')
obj.wind_direction = getattr(Direction, direction)
if 'Vitesse du vent' in self.titles:
speed_text = self.get_cell(self.titles['Vitesse du vent'], n)
......@@ -94,7 +70,15 @@ class WeatherPage(HTMLPage):
speed_text = self.get_cell(self.titles['Vitesse Moy. du vent'], n)
else:
speed_text = self.get_cell(self.titles['Vitesse moyenne du vent'], n)
obj.wind_speed = 1000 * int(speed_text.replace('km/h', '').strip())
obj.wind_speed = int(speed_text.replace('km/h', '').strip())
if 'Probabilité de précipitations' in self.titles:
txt = self.get_cell(self.titles['Probabilité de précipitations'], n)
obj.precipitation_probability = float(txt.strip('<%')) / 100
if 'Nébulosité' in self.titles:
txt = self.get_cell(self.titles['Nébulosité'], n).rstrip('%')
obj.cloud = int(round(float(txt) / 100 * 8))
class HourPage(WeatherPage):
......@@ -106,7 +90,7 @@ class HourPage(WeatherPage):
def get_current(self):
fore = next(iter(self.iter_forecast()))
ret = FullCurrent()
ret = Current()
for f in ('date', 'text', 'wind_direction', 'wind_speed', 'humidity'):
setattr(ret, f, getattr(fore, f))
ret.temp = fore.high
......@@ -121,7 +105,7 @@ class HourPage(WeatherPage):
day_str = None
for n in range(len(self.doc.xpath('//table[@id="meteoHour"]/tr[1]/td'))):
obj = FullForecast()
obj = Forecast()
t = time(int(self.get_cell(self.titles['Heure'], n).rstrip('h')), 0)
......@@ -152,7 +136,7 @@ class Days5Page(WeatherPage):
self.titles[CleanText('.')(tr)] = n
for n in range(1, len(self.doc.xpath('//table[@id="meteo2"]/tr[1]/td'))):
obj = FullForecast()
obj = Forecast()
obj.low = temp(int(self.get_cell(self.titles['Température Mini'], n).rstrip('°')))
obj.high = temp(int(self.get_cell(self.titles['Température Maxi'], n).rstrip('°')))
obj.date = d
......@@ -178,7 +162,7 @@ class Days10Page(WeatherPage):
cols = len(self.doc.xpath('//table[@id="meteo2"]//td/table'))
for n in range(1, cols):
obj = FullForecast()
obj = Forecast()
obj.low = temp(int(self.get_cell(self.titles['Température Mini'], n).rstrip('°C')))
obj.high = temp(int(self.get_cell(self.titles['Température Maxi'], n).rstrip('°C')))
obj.date = d
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment