Skip to content

Commit

Permalink
Merge pull request #56 from ssenart/develop
Browse files Browse the repository at this point in the history
[#55] Make the login more robust by using auth_nonce token.
  • Loading branch information
ssenart committed Nov 16, 2022
2 parents d6fe2d9 + 2c88db4 commit a6247fd
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 92 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/python-publish-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"

steps:

Expand Down Expand Up @@ -131,5 +136,5 @@ jobs:
python updateVersion.py --nextVersion ${{ steps.gitversion.outputs.majorMinorPatch }}.dev${{ steps.gitversion.outputs.buildMetaData }}
python setup.py sdist bdist_wheel --python-tag ${{ steps.python-tag.outputs.replaced }}
twine upload --skip-existing --repository testpypi dist/*
if: ${{ matrix.python-version == 3.8 && github.repository == 'ssenart/PyGazpar' }}
if: ${{ matrix.python-version == 3.10 && github.repository == 'ssenart/PyGazpar' }}

7 changes: 6 additions & 1 deletion .github/workflows/python-publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"

steps:

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.6](https://github.com/ssenart/PyGazpar/compare/1.1.5...1.1.6) - 2022-11-16
### Fixed
- [#55](https://github.com/ssenart/PyGazpar/issues/55): Problème de connexion.

## [1.1.5](https://github.com/ssenart/PyGazpar/compare/1.1.4...1.1.5) - 2022-07-11
### Fixed
- [#49](https://github.com/ssenart/PyGazpar/issues/49): Authentication failure.
Expand Down
1 change: 0 additions & 1 deletion pygazpar/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from pygazpar.enum import PropertyName, Frequency # noqa: F401
from pygazpar.client import Client # noqa: F401
from pygazpar.client import LoginError # noqa: F401
from pygazpar.version import __version__ # noqa: F401
39 changes: 24 additions & 15 deletions pygazpar/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
import requests
from pygazpar.enum import Frequency
from pygazpar.datafileparser import DataFileParser
from typing import Any, cast, List, Dict

AUTH_NONCE_URL = "https://monespace.grdf.fr/client/particulier/accueil"
LOGIN_URL = "https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth"
LOGIN_HEADER = {"domain": "grdf.fr"}
LOGIN_PAYLOAD = """{{
"email": "{0}",
"password": "{1}",
"capp": "meg",
"goto": "https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce=skywsNPCVa-AeKo1Rps0HjMVRNbUqA46j7XYA4tImeI&by_pass_okta=1&capp=meg"}}"""
"goto": "https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce={2}&by_pass_okta=1&capp=meg"}}"""
DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={1}&dateFin={2}&frequence={0}&pceList%5B%5D={3}"
DATA_FILENAME = 'Donnees_informatives_*.xlsx'

Expand All @@ -25,12 +27,6 @@
Logger = logging.getLogger(__name__)


# ------------------------------------------------------------------------------------------------------------
class LoginError(Exception):
""" Client has failed to login in GrDF Web site (check username/password)"""
pass


# ------------------------------------------------------------------------------------------------------------
class Client:

Expand All @@ -40,13 +36,13 @@ def __init__(self, username: str, password: str, pceIdentifier: str, meterReadin
self.__password = password
self.__pceIdentifier = pceIdentifier
self.__tmpDirectory = tmpDirectory
self.__data = []
self.__meterReadingFrequency = meterReadingFrequency
self.__lastNDays = lastNDays
self.__testMode = testMode
self.__data = []

# ------------------------------------------------------
def data(self) -> dict:
def data(self) -> List[Dict[str, Any]]:
return self.__data

# ------------------------------------------------------
Expand All @@ -71,7 +67,7 @@ def __updateTestMode(self):
dataSampleFilename = f"{os.path.dirname(os.path.abspath(__file__))}/resources/{dataSampleFilenameByFrequency[self.__meterReadingFrequency]}"

with open(dataSampleFilename) as jsonFile:
data = json.load(jsonFile)
data = cast(List[Dict[str, Any]], json.load(jsonFile))
self.__data = data
except Exception:
Logger.error("An unexpected error occured while loading sample data", exc_info=True)
Expand Down Expand Up @@ -137,18 +133,31 @@ def __updateLiveMode(self):
# ------------------------------------------------------
def __login(self, session: requests.Session):

payload = LOGIN_PAYLOAD.format(self.__username, self.__password)
# Get auth_nonce token.
session.get(AUTH_NONCE_URL)
if "auth_nonce" not in session.cookies:
raise Exception("Login error: Cannot get auth_nonce token")
auth_nonce = session.cookies.get("auth_nonce")

# Build the login payload as a json string.
payload = LOGIN_PAYLOAD.format(self.__username, self.__password, auth_nonce)

# Build the login payload as a python object.
data = json.loads(payload)

response = session.post('https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth', data=data)
# Send the login command.
response = session.post(LOGIN_URL, data=data)

# Check login result.
loginData = response.json()

response.raise_for_status()

loginData = response.json()
if "status" in loginData and "error" in loginData and loginData["status"] >= 400:
raise Exception(f"{loginData['error']} ({loginData['status']})")

if loginData["state"] != "SUCCESS":
raise LoginError()
if "state" in loginData and loginData["state"] != "SUCCESS":
raise Exception(loginData["error"])

# ------------------------------------------------------
def __downloadFile(self, session: requests.Session, url: str, path: str):
Expand Down
38 changes: 34 additions & 4 deletions pygazpar/clientV2.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import requests
import datetime
import json
from typing import Dict

DEFAULT_LAST_N_DAYS = 365

Expand All @@ -12,7 +14,7 @@
class ClientV2:

# ------------------------------------------------------
def __init__(self, username: str, password: str, pceIdentifier: int, lastNDays: int = DEFAULT_LAST_N_DAYS, testMode: bool = False):
def __init__(self, username: str, password: str, pceIdentifier: str, lastNDays: int = DEFAULT_LAST_N_DAYS, testMode: bool = False):
self.__username = username
self.__password = password
self.__pceIdentifier = pceIdentifier
Expand All @@ -21,7 +23,7 @@ def __init__(self, username: str, password: str, pceIdentifier: int, lastNDays:
self.__data = {}

# ------------------------------------------------------
def data(self) -> dict:
def data(self) -> Dict:
return self.__data

# ------------------------------------------------------
Expand All @@ -44,14 +46,42 @@ def __updateLiveMode(self):

session = requests.Session()

session.headers.update(
{
"User-Agent": "Mozilla/5.0"
" (Linux; Android 6.0; Nexus 5 Build/MRA58N)"
" AppleWebKit/537.36 (KHTML, like Gecko)"
" Chrome/61.0.3163.100 Mobile Safari/537.36",
"Accept-Encoding": "gzip, deflate, br",
"Accept": "application/json, */*",
"Connection": "keep-alive",
"domain": "grdf.fr",
}
)

# Get auth_nonce cookie.
_ = session.get("https://monespace.grdf.fr/client/particulier/accueil")
if "auth_nonce" not in session.cookies:
raise Exception("Cannot get auth_nonce.")
auth_nonce = session.cookies.get("auth_nonce")

# Login
session.post('https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth', data={
loginResponse = session.post('https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth', data={
'email': self.__username,
'password': self.__password,
'capp': 'meg',
'goto': 'https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce=skywsNPCVa-AeKo1Rps0HjMVRNbUqA46j7XYA4tImeI&by_pass_okta=1&capp=meg'
'goto': f"https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce={auth_nonce}&by_pass_okta=1&capp=meg"
})

# Check login result.
loginData = json.loads(loginResponse.text)

if "status" in loginData and "error" in loginData and loginData["status"] >= 400:
raise Exception(f"{loginData['error']} ({loginData['status']})")

if "state" in loginData and loginData["state"] != "SUCCESS":
raise Exception(loginData["error"])

# Build URL to get the data from.
dateFormat = "%Y-%m-%d"
endDate = datetime.date.today()
Expand Down
53 changes: 23 additions & 30 deletions pygazpar/datafileparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.cell.cell import Cell
from openpyxl import load_workbook
from typing import Any, List, Dict


FIRST_DATA_LINE_NUMBER = 10
Expand All @@ -17,7 +18,7 @@ class DataFileParser:

# ------------------------------------------------------
@staticmethod
def parse(dataFilename: str, dataReadingFrequency: Frequency) -> list:
def parse(dataFilename: str, dataReadingFrequency: Frequency) -> List[Dict[str, Any]]:

parseByFrequency = {
Frequency.HOURLY: DataFileParser.__parseHourly,
Expand All @@ -26,18 +27,10 @@ def parse(dataFilename: str, dataReadingFrequency: Frequency) -> list:
Frequency.MONTHLY: DataFileParser.__parseMonthly
}

# worksheetNameByFrequency = {
# Frequency.HOURLY: "Historique par heure",
# Frequency.DAILY: "Historique par jour",
# Frequency.WEEKLY: "Historique par semaine",
# Frequency.MONTHLY: "Historique par mois"
# }

DataFileParser.logger.debug(f"Loading Excel data file '{dataFilename}'...")

workbook = load_workbook(filename=dataFilename)

# worksheet = workbook[worksheetNameByFrequency[dataReadingFrequency]]
worksheet = workbook.active

res = parseByFrequency[dataReadingFrequency](worksheet)
Expand All @@ -48,26 +41,26 @@ def parse(dataFilename: str, dataReadingFrequency: Frequency) -> list:

# ------------------------------------------------------
@staticmethod
def __fillRow(row: dict, propertyName: str, cell: Cell, isNumber: bool):
def __fillRow(row: Dict, propertyName: str, cell: Cell, isNumber: bool):

if cell.value is not None:
if isNumber:
if type(cell.value) is str:
if len(cell.value.strip()) > 0:
row[propertyName] = float(cell.value.replace(',', '.'))
else:
row[propertyName] = float(cell.value)
row[propertyName] = cell.value
else:
row[propertyName] = cell.value
row[propertyName] = cell.value.strip() if type(cell.value) is str else cell.value

# ------------------------------------------------------
@staticmethod
def __parseHourly(worksheet: Worksheet) -> list:
def __parseHourly(worksheet: Worksheet) -> List[Dict[str, Any]]:
return []

# ------------------------------------------------------
@staticmethod
def __parseDaily(worksheet: Worksheet) -> list:
def __parseDaily(worksheet: Worksheet) -> List[Dict[str, Any]]:

res = []

Expand All @@ -79,14 +72,14 @@ def __parseDaily(worksheet: Worksheet) -> list:
for rownum in range(minRowNum, maxRowNum + 1):
row = {}
if worksheet.cell(column=2, row=rownum).value is not None:
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False)
DataFileParser.__fillRow(row, PropertyName.START_INDEX.value, worksheet.cell(column=3, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.END_INDEX.value, worksheet.cell(column=4, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=5, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=6, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.CONVERTER_FACTOR.value, worksheet.cell(column=7, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.TEMPERATURE.value, worksheet.cell(column=8, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.TYPE.value, worksheet.cell(column=9, row=rownum), False)
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
DataFileParser.__fillRow(row, PropertyName.START_INDEX.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.END_INDEX.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=5, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=6, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.CONVERTER_FACTOR.value, worksheet.cell(column=7, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.TEMPERATURE.value, worksheet.cell(column=8, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.TYPE.value, worksheet.cell(column=9, row=rownum), False) # type: ignore
row[PropertyName.TIMESTAMP.value] = data_timestamp
res.append(row)

Expand All @@ -96,7 +89,7 @@ def __parseDaily(worksheet: Worksheet) -> list:

# ------------------------------------------------------
@staticmethod
def __parseWeekly(worksheet: Worksheet) -> list:
def __parseWeekly(worksheet: Worksheet) -> List[Dict[str, Any]]:

res = []

Expand All @@ -108,9 +101,9 @@ def __parseWeekly(worksheet: Worksheet) -> list:
for rownum in range(minRowNum, maxRowNum + 1):
row = {}
if worksheet.cell(column=2, row=rownum).value is not None:
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False)
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
row[PropertyName.TIMESTAMP.value] = data_timestamp
res.append(row)

Expand All @@ -120,7 +113,7 @@ def __parseWeekly(worksheet: Worksheet) -> list:

# ------------------------------------------------------
@staticmethod
def __parseMonthly(worksheet: Worksheet) -> list:
def __parseMonthly(worksheet: Worksheet) -> List[Dict[str, Any]]:

res = []

Expand All @@ -132,9 +125,9 @@ def __parseMonthly(worksheet: Worksheet) -> list:
for rownum in range(minRowNum, maxRowNum + 1):
row = {}
if worksheet.cell(column=2, row=rownum).value is not None:
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False)
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True)
DataFileParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
DataFileParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
DataFileParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
row[PropertyName.TIMESTAMP.value] = data_timestamp
res.append(row)

Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
flake8
pytest
openpyxl
requests
pytest
8 changes: 3 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@ classifiers =
Development Status :: 5 - Production/Stable
Topic :: Software Development :: Libraries
Operating System :: OS Independent
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10

[options]
zip_safe = False
include_package_data = True
packages = find:
python_requires = >=3.5
python_requires = >=3.9
install_requires =
openpyxl >= 2.6.3
requests >= 2.26.0
Expand Down
Loading

0 comments on commit a6247fd

Please sign in to comment.