diff --git a/modules/ekwateur/__init__.py b/modules/ekwateur/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c85718c8d05a53e88d6f1bf69332fb47a174efb0
--- /dev/null
+++ b/modules/ekwateur/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Phyks (Lucas Verney)
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from .module import EkwateurModule
+
+
+__all__ = ['EkwateurModule']
diff --git a/modules/ekwateur/browser.py b/modules/ekwateur/browser.py
new file mode 100644
index 0000000000000000000000000000000000000000..e960a1d57f6bb235edc48544eba026dcf6c0644d
--- /dev/null
+++ b/modules/ekwateur/browser.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Phyks (Lucas Verney)
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+import itertools
+
+
+from weboob.browser import LoginBrowser, need_login, URL
+from weboob.exceptions import BrowserIncorrectPassword
+
+from .pages import BillsPage, DocumentsPage, LoginPage
+
+
+class EkwateurBrowser(LoginBrowser):
+ BASEURL = 'https://mon-espace.ekwateur.fr/'
+
+ login_page = URL('/se_connecter', LoginPage)
+ bills_page = URL('/mes_factures_et_acomptes', BillsPage)
+ documents_page = URL('/documents', DocumentsPage)
+
+ def do_login(self):
+ self.login_page.go().do_login(self.username, self.password)
+ self.bills_page.stay_or_go()
+ if not self.bills_page.is_here():
+ raise BrowserIncorrectPassword
+
+ @need_login
+ def iter_subscriptions(self):
+ return self.bills_page.stay_or_go().get_subscriptions()
+
+ @need_login
+ def iter_documents(self, sub_id):
+ return itertools.chain(
+ self.documents_page.stay_or_go().get_documents(sub_id=sub_id),
+ self.documents_page.stay_or_go().get_cgv(sub_id),
+ self.documents_page.stay_or_go().get_justificatif(sub_id),
+ self.bills_page.stay_or_go().get_bills(sub_id=sub_id)
+ )
diff --git a/modules/ekwateur/module.py b/modules/ekwateur/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..004900b2645a0f0f417d58fa08513bedf4d0d312
--- /dev/null
+++ b/modules/ekwateur/module.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Phyks (Lucas Verney)
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.tools.backend import Module, BackendConfig
+from weboob.tools.value import Value, ValueBackendPassword
+from weboob.capabilities.base import find_object
+from weboob.capabilities.bill import (
+ CapDocument, Document, DocumentNotFound, Subscription
+)
+
+from .browser import EkwateurBrowser
+
+
+__all__ = ['EkwateurModule']
+
+
+class EkwateurModule(Module, CapDocument):
+ NAME = 'ekwateur'
+ DESCRIPTION = 'ekwateur website'
+ MAINTAINER = 'Phyks (Lucas Verney)'
+ EMAIL = 'phyks@phyks.me'
+ LICENSE = 'AGPLv3+'
+ VERSION = '1.4'
+
+ BROWSER = EkwateurBrowser
+
+ CONFIG = BackendConfig(
+ Value('login', help='Email or identifier'),
+ ValueBackendPassword('password', help='Password'),
+ )
+
+ def create_default_browser(self):
+ return self.create_browser(self.config['login'].get(), self.config['password'].get())
+
+ def get_document(self, id):
+ """
+ Get a document.
+
+ :param id: ID of document
+ :rtype: :class:`Document`
+ :raises: :class:`DocumentNotFound`
+ """
+ return find_object(
+ self.iter_documents(id.split("#")[-1]),
+ id=id,
+ error=DocumentNotFound
+ )
+
+ def download_document(self, doc):
+ if not isinstance(doc, Document):
+ doc = self.get_document(doc)
+
+ if not doc.url:
+ return None
+
+ return self.browser.open(doc.url).content
+
+ def iter_documents(self, subscription):
+ """
+ Iter documents.
+
+ :param subscription: subscription to get documents
+ :type subscription: :class:`Subscription`
+ :rtype: iter[:class:`Document`]
+ """
+ if isinstance(subscription, Subscription):
+ subscription = subscription.id
+ return self.browser.iter_documents(subscription)
+
+ def iter_subscription(self):
+ """
+ Iter subscriptions.
+
+ :rtype: iter[:class:`Subscription`]
+ """
+ return self.browser.iter_subscriptions()
diff --git a/modules/ekwateur/pages.py b/modules/ekwateur/pages.py
new file mode 100644
index 0000000000000000000000000000000000000000..46e47682fb50a7f75e3884c25f7083b7c5957235
--- /dev/null
+++ b/modules/ekwateur/pages.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Phyks (Lucas Verney)
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.browser.elements import (
+ ItemElement, ListElement, TableElement, method
+)
+from weboob.browser.pages import HTMLPage
+from weboob.browser.filters.standard import (
+ Date, CleanDecimal, CleanText, Currency, Env, Format, Regexp, Slugify,
+ TableCell
+)
+from weboob.browser.filters.html import AbsoluteLink, Attr, Link, XPath
+from weboob.capabilities.base import NotAvailable
+from weboob.capabilities.bill import Subscription, Bill, Document
+
+
+class LoginPage(HTMLPage):
+ def do_login(self, login, password):
+ form = self.get_form(nr=0)
+ form['_username'] = login
+ form['_password'] = password
+ form.submit()
+
+
+class EkwateurPage(HTMLPage):
+ @property
+ def logged(self):
+ return bool(self.doc.xpath(u'//*[has-class("menu__user__infos")]'))
+
+
+class BillsPage(EkwateurPage):
+ @method
+ class get_subscriptions(ListElement):
+ item_xpath = '//a[has-class("nom_contrat")]'
+
+ class item(ItemElement):
+ klass = Subscription
+
+ def obj_id(self):
+ return Link('.')(self).split('/')[-1]
+
+ def obj_label(self):
+ name = Attr('.', 'data-nomcontrat', default=None)(self)
+ if not name:
+ name = CleanText('.')(self)
+ return name
+
+ @method
+ class get_bills(TableElement):
+ item_xpath = '//table[@id="dataHistorique"]/tbody/tr'
+ head_xpath = '//table[@id="dataHistorique"]/thead/tr/td/text()'
+
+ col_date = 'Date'
+ col_type = 'Type'
+ col_amount = 'Montant'
+ col_status = 'Statut'
+
+ class item(ItemElement):
+ klass = Bill
+
+ obj_id = Format(
+ 'facture-%s-%s-%s#%s',
+ Slugify(CleanText(TableCell('date'))),
+ Slugify(CleanText(TableCell('amount'))),
+ Slugify(CleanText(TableCell('type'))),
+ Env('sub_id')
+ )
+ obj_url = AbsoluteLink('./td[5]//a', default=NotAvailable)
+ obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
+ obj_label = Format(
+ '%s %s %s',
+ CleanText(TableCell('type')),
+ CleanText(TableCell('amount')),
+ CleanText(TableCell('date'))
+ )
+ obj_type = 'bill'
+ obj_price = CleanDecimal(TableCell('amount'), replace_dots=True)
+ obj_currency = Currency(TableCell('amount'))
+ obj_duedate = Date(
+ Regexp(
+ CleanText(TableCell('status')),
+ r'le (\d+)/(\d+)/(\d+)',
+ r'\1/\2/\3'
+ ),
+ dayfirst=True
+ )
+
+ def obj_format(self):
+ if self.obj_url(self):
+ return 'pdf'
+ return NotAvailable
+
+ def obj_income(self):
+ if self.obj_price(self) < 0:
+ return True
+ return False
+
+
+class DocumentsPage(EkwateurPage):
+ @method
+ class get_documents(TableElement):
+ item_xpath = '//table[@id="otherDocuments"]/tbody/tr'
+ head_xpath = '//table[@id="otherDocuments"]/thead/tr/td/text()'
+
+ col_date = 'Date'
+ col_type = 'Type'
+
+ class item(ItemElement):
+ klass = Document
+
+ obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
+ obj_format = 'pdf'
+ obj_label = CleanText(TableCell('type'))
+ obj_url = AbsoluteLink('./td[3]//a', default=NotAvailable)
+ obj_id = Format(
+ 'doc-%s-%s#%s',
+ Slugify(CleanText(TableCell('date'))),
+ Slugify(CleanText(TableCell('type'))),
+ Env('sub_id')
+ )
+
+ def get_justificatif(self, sub_id):
+ doc = Document()
+ doc.id = 'doc-justificatif#%s' % sub_id
+ doc.format = 'pdf'
+ doc.date = NotAvailable
+ doc.label = 'Justificatif de domicile'
+ doc.url = 'https://mon-espace.ekwateur.fr/client/justificatif_de_domicile'
+ yield doc
+
+ def get_cgv(self, sub_id):
+ CGV = Document()
+ CGV.id = 'doc-CGV#%s' % sub_id
+ CGV.format = 'pdf'
+ CGV.date = NotAvailable
+ CGV.label = 'CGV électricité'
+ CGV.type = 'cgv'
+ for item in XPath(
+ './/div[has-class("table__foot__adobe-reader__link")]//a'
+ )(self.doc):
+ if 'CGV' in item.text:
+ CGV.url = item.attrib['href']
+ yield CGV
diff --git a/modules/ekwateur/test.py b/modules/ekwateur/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..98b18b815f4e0222c42999c4832b66e7f06c434c
--- /dev/null
+++ b/modules/ekwateur/test.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Phyks (Lucas Verney)
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.tools.test import BackendTest
+
+
+class EkwateurTest(BackendTest):
+ MODULE = 'ekwateur'
+
+ def test_document(self):
+ subscriptions = list(self.backend.iter_subscription())
+ assert subscriptions
+
+ for sub in subscriptions:
+ docs = list(self.backend.iter_documents(sub))
+ assert docs
+
+ for doc in docs:
+ content = self.backend.download_document(doc)
+ assert content
diff --git a/tools/py3-compatible.modules b/tools/py3-compatible.modules
index a3ed9b6a5f7a5dd7f7633d6820a149de4f8656cd..0850053873c68f0e7f3b689831baa98480cd5f85 100644
--- a/tools/py3-compatible.modules
+++ b/tools/py3-compatible.modules
@@ -43,6 +43,7 @@ delubac
dlfp
ebonics
edf
+ekwateur
entreparticuliers
erehsbc
esalia