Commit 221a1cf6 authored by Rémi FERRAND ⛰'s avatar Rémi FERRAND ⛰
Browse files

Merge branch 'release/0.1.0'

parents 5f7dcc72 047fca1f
This diff is collapsed.
# FREE ADSL Bill fetcher
#### Table of Contents
1. [Overview](#overview)
2. [Instructions](#instructions)
* [How to install](#how_to_install)
* [How to configure](#how_to_configure)
* [How to use](#how_to_use)
3. [Development - Guide for contributing](#development)
## Overview
This software was created to retrieve FREE telecom bills in an easy way.
## Instructions
### How to install
```
$ git clone git@gitlab.in2p3.fr:rferrand/free_adsl_bill_fetcher.git
$ pip install -r free_adsl_bill_fetcher/requirements.txt
```
### How to configure
Software configuration file defaults to `~/.free_adsl_bill_fetcher.conf`.
**Example**:
```json
{
"login": "TheLog1n",
"password": "thePassw0rd"
}
```
#### `login`
Your FREE telecom username.
#### `password`
Your FREE telecom account password.
### How to use
```
Usage: free_adsl_bill_fetcher [options]
Options:
-h, --help show this help message and exit
-p, --show-price show price when listing bills
--latest only fetch latest bill
-c FILE, --config=FILE
configuration file
--get=BILL_TITLE Download bill BILL_TITLE and write it as
BILL_TITLE.pdf
-d DIR, --write-dir=DIR
write bills to directory DIR
--name-prefix=STR prefix each bill filename with STR
--name-suffix=STR suffix each bill filename with STR (before PDF
extension)
```
**Retrieve latest bill**
```
$ free_adsl_bill_fetcher --latest
Your latest bill was written to './Octobre_2014.pdf'
```
**List all available bills with their price**
```
$ free_adsl_bill_fetcher -p
+----------------+---------+
| Month | Price |
+----------------+---------+
| Octobre 2014 | 12 € |
| ... | ....... |
+----------------+---------+
```
**Retrieve multiple specific bills**
```
$ free_adsl_bill_fetcher --get='Août 2014' --get='Août 2013'
[*] Août 2014 bill was written to './Août_2014.pdf'
[*] Août 2013 bill was written to './Août_2013.pdf'
```
**Add suffix and/or prefix to bills names**
```
$ free_adsl_bill_fetcher --get='Août 2014' --get='Août 2013' --name-prefix="Ferrand_Remi_" --name-suffix="_Facture_ADSL"
[*] Août 2014 bill was written to './Ferrand_Remi_Août_2014_Facture_ADSL.pdf'
[*] Août 2013 bill was written to './Ferrand_Remi_Août_2013_Facture_ADSL.pdf'
```
## Development - Guide for contributing
Please submit a Pull Request / Merge request if you want to contribute to this project.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) Remi Ferrand <remi.ferrand_at_riton.fr>, 2014
#
# This software is a computer program whose purpose is to [describe
# functionalities and technical features of your software].
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
#
import re
import os
import sys
import json
import logging
import requests
import HTMLParser
from optparse import OptionParser
from prettytable import PrettyTable
# try:
# import http.client as http_client
# except ImportError:
# import httplib as http_client
#
# http_client.HTTPConnection.debuglevel = 1
#
# logging.basicConfig()
# logging.getLogger().setLevel(logging.DEBUG)
# requests_log = logging.getLogger("requests.packages.urllib3")
# requests_log.setLevel(logging.DEBUG)
# requests_log.propagate = True
class HTTPClient(object):
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36'
def __init__(this):
pass
def _default_headers(this):
return {
'User-Agent': this.USER_AGENT
}
def get(this, url, headers={}, *args, **kwargs):
h = this._default_headers().copy()
headers.update(h)
return requests.get(url,
headers=headers,
*args, **kwargs)
def post(this, url, headers={}, *args, **kwargs):
h = this._default_headers().copy()
headers.update(h)
return requests.post(url,
headers=headers,
*args, **kwargs)
@staticmethod
def unescapeHTML(text):
return HTMLParser.HTMLParser().unescape(text)
class FreeAdslBillFetcher(HTTPClient):
LOGIN_URL = 'https://subscribe.free.fr/login/login.pl'
LOGOUT_URL = 'https://adsl.free.fr/logout.pl'
BILL_ROOT_URL = 'https://adsl.free.fr'
LIST_BILLS_URL = BILL_ROOT_URL + '/liste-factures.pl'
BILLFINDER_RE = re.compile(r'<strong>(?P<month>\w+?\s*\d{4})</strong>.*?<strong>(?P<price>\d+(?:\.\d+)?) Euros</strong>.*?<a href="(?P<url>facture_pdf.pl.+?)"', re.S | re.UNICODE)
class FreeSessionCredentials(object):
def __init__(this, id, idt):
this.id = id
this.idt = idt
def toDict(this):
return {'id': this.id, 'idt': this.idt}
def __str__(this):
return "id=%s&idt=%s" % (this.id, this.idt)
class FreeAdslBill(HTTPClient):
def __init__(this, title, amount, url):
this.title = title
this.amount = amount
this.url = url
def __eq__(this, other):
return this.title == other.title
def __repr__(this):
return "FreeAdslBill<%s>" % this.title.encode('utf-8')
def __init__(this, user, password):
this.user = user
this.password = password
this._transaction_creds = None
def login(this):
payload = {
'login': this.user,
'pass' : this.password,
'ok' : 'Valider',
'link' : ''
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
r = this.post(this.LOGIN_URL, headers=headers, data=payload,
allow_redirects=False)
location = r.headers['location']
this._buildSessionCreds(location)
def _buildSessionCreds(this, url):
# https://adsl.free.fr/home.pl?id=16779576&idt=1015c62833046b0f
ids_s = url[url.index('?')+1:]
h = dict(x.split('=') for x in ids_s.split('&'))
this._transaction_creds = FreeAdslBillFetcher.FreeSessionCredentials(id=h['id'], idt=h['idt'])
def _appendUrlCreds(this, url):
creds = this._transaction_creds
return url + '?' + str(creds)
def listBills(this):
if this._transaction_creds is None: this.login()
# We need to build the URL/params by hand.
# python-requests params doesn't work here
# it seems that url parameters are order dependant
# on this webservice
url = this._appendUrlCreds(this.LIST_BILLS_URL)
r = this.get(url, allow_redirects=False)
bills = this._parseBillsList(this.unescapeHTML(r.text))
return bills
def fetchBill(this, bill):
# assume that whole PDF fits in memory
return this.get(bill.url).content
def writeBillToFile(this, bill, file):
# assume that whole PDF fits in memory
with open(file, 'wb') as f:
f.write(this.fetchBill(bill))
def _parseBillsList(this, body):
match = this.BILLFINDER_RE.findall(body)
bills = []
for month, price, url in match:
bills.append(FreeAdslBillFetcher.FreeAdslBill(month, float(price), this.BILL_ROOT_URL + '/' + url))
return bills
def logout(this):
if this._transaction_creds is None:
return
url = this._appendUrlCreds(this.LOGOUT_URL)
this.get(url, allow_redirects=False)
def __enter__(this):
this.login()
return this
def __exit__(this, type, value, traceback):
this.logout()
class FreeAdslBillFetcherCli(object):
PROG_NAME = 'free_adsl_bil_fetcher'
@staticmethod
def _buildOptParser():
parser = OptionParser()
parser.add_option("-p", "--show-price", dest="show_price", default=False,
action="store_true", help="show price when listing bills")
parser.add_option("--latest", dest="fetch_latest", default=False,
action="store_true", help="only fetch latest bill")
parser.add_option("-c", "--config", dest="config_file",
default=os.path.expanduser('~') + '/.' + FreeAdslBillFetcherCli.PROG_NAME + '.conf',
metavar='FILE', help="configuration file")
parser.add_option("--get", dest="wanted_bills",
action="append",
default=[],
metavar='BILL_TITLE', help="Download bill BILL_TITLE and write it as BILL_TITLE.pdf")
parser.add_option("-d", "--write-dir", dest="write_directory",
default='.',
metavar='DIR', help="write bills to directory DIR")
parser.add_option("--name-prefix", dest="name_prefix",
default='',
metavar='STR', help="prefix each bill filename with STR")
parser.add_option("--name-suffix", dest="name_suffix",
default='',
metavar='STR', help="suffix each bill filename with STR (before PDF extension)")
return parser
def parseArgs(this, arg):
(options, args) = this.opt_parser.parse_args(args=arg)
this.options = options
this.args = args
def __init__(this, args=sys.argv[1:]):
this.opt_parser = this._buildOptParser()
this.options = None
this.args = None
this.parseArgs(args)
this.config = this._parseConfigFile(this.options.config_file)
this.fetcher = None
this.table = this._prepareTable()
def __enter__(this, *args, **kwargs):
this.fetcher = FreeAdslBillFetcher(this.config['login'], this.config['password'])
this.fetcher.login()
return this
def __exit__(this, type, value, traceback):
this.fetcher.logout()
@staticmethod
def _parseConfigFile(file):
with open(file) as f:
return json.load(f)
def _prepareTable(this):
headers = ['Month']
if this.options.show_price:
headers.insert(1, 'Price')
pt = PrettyTable(headers)
pt.align['Month'] = 'l'
return pt
def run(this):
wanted_bills = len(this.options.wanted_bills)
if wanted_bills != 0 or this.options.fetch_latest:
table_output = False
else:
table_output = True
for bill in this.fetcher.listBills():
if this.options.fetch_latest is True:
pdf_path = this._fetchBill(bill)
print "Your latest bill was written to '%s'" % pdf_path
break
if wanted_bills > 0:
if bill.title in [x.decode('utf-8') for x in this.options.wanted_bills]:
pdf_path = this._fetchBill(bill)
print "[*] %s bill was written to '%s'" % (bill.title, pdf_path)
else:
this._appendBillToTable(bill)
if table_output:
print this.table
sys.exit(0)
def _appendBillToTable(this, bill):
row = [bill.title]
if this.options.show_price:
row.insert(1, '%.2f €' % bill.amount)
this.table.add_row(row)
def _composeBillFilename(this, bill):
bill_filename = this.options.write_directory.rstrip('/') + '/'
bill_filename += this.options.name_prefix
bill_filename += re.sub(r'\s+', '_', bill.title)
bill_filename += this.options.name_suffix + '.pdf'
return bill_filename
def _fetchBill(this, bill):
bill_filename = this._composeBillFilename(bill)
this.fetcher.writeBillToFile(bill, bill_filename)
return bill_filename
if __name__ == "__main__":
with FreeAdslBillFetcherCli() as cli:
cli.run()
#!/usr/bin/env python
import os
import sys
import json
import requests
class FreeAdslFetcher(object):
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36'
LOGIN_URL = 'https://subscribe.free.fr/login/'
def __init__(this, user, password):
this.user = user
this.password = password
this._auth_cookie = None
def _default_headers(this):
return {
'User-Agent': this.USER_AGENT
}
def _login(this):
payload = {
'login': this.user,
'pass' : this.password
}
r = requests.post(this.LOGIN_URL, headers=this._default_headers(), data=payload)
print r.text
def main():
ids = None
with open('./ids.json') as f:
ids = json.load(f)
fetcher = FreeAdslFetcher(ids['login'], ids['password'])
if __name__ == "__main__":
main()
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