Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
C
crawler
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Project - Tarifs Crawler & API
crawler
Commits
fc69800a
Commit
fc69800a
authored
May 12, 2025
by
Marco Schmiedel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
ccf805f2
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
134 additions
and
104 deletions
+134
-104
AWSConfig.py
config/AWSConfig.py
+1
-1
MauiConfig.py
config/MauiConfig.py
+9
-0
WebManager.py
manager/WebManager.py
+1
-1
EeccxRouter.py
routes/EeccxRouter.py
+122
-101
HealtCheckRouter.py
routes/HealtCheckRouter.py
+1
-1
No files found.
config/AWSConfig.py
View file @
fc69800a
AWS_ACCESS_KEY_ID
=
"AKIATXO2FKVK3BSS3DMT"
AWS_ACCESS_KEY_ID
=
"AKIATXO2FKVK3BSS3DMT"
AWS_SECRET_ACCESS_KEY
=
"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"
AWS_SECRET_ACCESS_KEY
=
"u6CvzjBJCo6qiL0zj8txOVGRUDlsspyhfLU/YK+Q"
REGION
=
"eu-central-1"
REGION
=
"eu-central-1"
BUCKET_NAME
=
"
freenetflyer
"
BUCKET_NAME
=
"
compactcdn.backoffice.online
"
config/MauiConfig.py
View file @
fc69800a
MAUI_USERNAME
=
"28009594-198"
MAUI_USERNAME
=
"28009594-198"
MAUI_PASSWORD
=
"8v#5YeeQyh"
MAUI_PASSWORD
=
"8v#5YeeQyh"
MAUI_AUTHCODE
=
"2D3JJNG3WWGSWRDI5KRW2MZKL3NJEZXJ"
MAUI_AUTHCODE
=
"2D3JJNG3WWGSWRDI5KRW2MZKL3NJEZXJ"
EECCX_TOKEN_URL
=
"https://sts.md.de/v1/oidc/token"
EECCX_API_URL
=
"https://partner-api.md.de/vertragserfassung/ftpOption2Pci.php"
EECCX_CLIENT_ID
=
"8VyjbQZyTVdx2T2UO6mA3ZTEeiodHcp-"
EECCX_CLIENT_SECRET
=
"FTE3y3Hj3TXeYrR8JbbO9yRRJ5ZGmPPTHH4HpyLFd9_X6wOx"
EECCX_CF_CLIENT_ID
=
"e42d165bde7363f8478a157b57425fd5.access"
EECCX_CF_CLIENT_SECRET
=
"de141ba4a6fbf9c29f51ba86fce9e81d3479797ff010a7ba43cde01977eac565"
EECCX_HDL_NR
=
28009594
EECCX_PROV_HDL_NR
=
28009594
manager/WebManager.py
View file @
fc69800a
...
@@ -61,7 +61,7 @@ def _require_token():
...
@@ -61,7 +61,7 @@ def _require_token():
token
=
request
.
args
.
get
(
"token"
)
token
=
request
.
args
.
get
(
"token"
)
if
token
!=
TOKEN_VALUE
:
if
token
!=
TOKEN_VALUE
:
return
(
return
(
jsonify
({
"
status"
:
"ERROR"
,
"
message"
:
"Please enter a valid token."
}),
jsonify
({
"message"
:
"Please enter a valid token."
}),
401
,
401
,
)
)
...
...
routes/EeccxRouter.py
View file @
fc69800a
"""
#!/usr/bin/env python3
Health-Check-Router (Caching & PDF-Rückgabe)
# -*- coding: utf-8 -*-
Stellt **einen** Endpunkt bereit
--------------------------------
GET /eeccx/<id>?options=A,B,C
GET /eeccx/<id>?options=A&options=B…
Workflow
--------
1. ID + Options → SHA-256-Hash → ./cache/<hash>.pdf
• Datei vorhanden ⇒ PDF sofort senden.
2. OAuth-Token holen (client-credentials).
3. Partner-API aufrufen.
• Enthält die Antwort ein Feld **error** → Fehlerbotschaft(en)
per JSON an den Client weitergeben.
• Enthält die Antwort kein *pcsPdf/pciPdf* → Fehler aus Antwort
oder Standardmeldung zurückgeben.
4. PDF herunterladen, cachen, ausliefern.
5. Jeder andere Fehler liefert immer
`{"status":"ERROR","message":"…"}`
– **ohne** HTML-Escaping/Unicode-Escapes („ä“, „ü“ usw. bleiben sichtbar).
Hinweis
-------
• SSL-Verifikation ist zu Demo-Zwecken deaktiviert (`verify=False`).
• Die Secrets stammen 1-zu-1 aus Deinem Beispielskript.
"""
from
__future__
import
annotations
from
__future__
import
annotations
import
sys
import
sys
sys
.
path
.
append
(
".."
)
sys
.
path
.
append
(
".."
)
import
time
import
hashlib
import
hashlib
import
io
import
io
import
json
import
json
import
os
import
os
import
tempfile
from
typing
import
List
,
Tuple
from
typing
import
List
,
Tuple
import
requests
import
requests
import
urllib3
import
urllib3
from
flask
import
Blueprint
,
Response
,
request
,
send_file
from
flask
import
Blueprint
,
Response
,
request
from
sqlalchemy.orm
import
joinedload
from
config.MauiConfig
import
EECCX_TOKEN_URL
,
EECCX_API_URL
,
EECCX_CLIENT_ID
,
EECCX_CLIENT_SECRET
,
EECCX_CF_CLIENT_ID
,
EECCX_CF_CLIENT_SECRET
,
EECCX_HDL_NR
,
EECCX_PROV_HDL_NR
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
#
Warnungen zu unsicheren HTTPS-Requests unterdrücken (nur Dev)
#
#
Eigene Module
#
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
urllib3
.
disable_warnings
(
urllib3
.
exceptions
.
InsecureRequestWarning
)
from
manager.S3Manager
import
S3Manager
from
manager.MysqlManager
import
MysqlManager
from
models.deal_deal
import
DealDeal
from
models.base_base
import
BaseBase
from
models.option_opti
import
OptionOpti
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
#
Blueprint
#
#
Warnungen zu unsicheren HTTPS-Requests unterdrücken (nur Dev)
#
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
blueprint
=
Blueprint
(
__name__
.
rsplit
(
"."
,
1
)[
-
1
],
__name__
)
urllib3
.
disable_warnings
(
urllib3
.
exceptions
.
InsecureRequestWarning
)
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
# Konfiguration / Konstanten #
# Konfiguration / Konstanten #
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
TOKEN_URL
=
"https://sts.md.de/v1/oidc/token"
TOKEN_URL
=
EECCX_TOKEN_URL
API_URL
=
"https://partner-api.md.de/vertragserfassung/ftpOption2Pci.php"
API_URL
=
EECCX_API_URL
CLIENT_ID
=
EECCX_CLIENT_ID
CLIENT_ID
=
"8VyjbQZyTVdx2T2UO6mA3ZTEeiodHcp-"
CLIENT_SECRET
=
EECCX_CLIENT_SECRET
CLIENT_SECRET
=
"FTE3y3Hj3TXeYrR8JbbO9yRRJ5ZGmPPTHH4HpyLFd9_X6wOx"
CF_CLIENT_ID
=
EECCX_CF_CLIENT_ID
CF_CLIENT_SECRET
=
EECCX_CF_CLIENT_SECRET
CF_CLIENT_ID
=
"e42d165bde7363f8478a157b57425fd5.access"
HDL_NR
=
EECCX_HDL_NR
CF_CLIENT_SECRET
=
"de141ba4a6fbf9c29f51ba86fce9e81d3479797ff010a7ba43cde01977eac565"
PROV_HDL_NR
=
EECCX_PROV_HDL_NR
HDL_NR
=
28009594
PROV_HDL_NR
=
28009594
PRODUKT_KATEGORIE
=
"O"
PRODUKT_KATEGORIE
=
"O"
CACHE_DIR
=
"./../cache"
# --------------------------------------------------------------------------- #
# Blueprint #
# --------------------------------------------------------------------------- #
blueprint
=
Blueprint
(
__name__
.
rsplit
(
"."
,
1
)[
-
1
],
__name__
)
# S3-Manager für Uploads
s3_manager
=
S3Manager
()
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
# Hilfsfunktionen #
# Hilfsfunktionen #
# --------------------------------------------------------------------------- #
# --------------------------------------------------------------------------- #
def
_json_error
(
message
:
str
,
status_code
:
int
=
500
)
->
Response
:
def
_json_error
(
message
:
str
,
status_code
:
int
=
500
)
->
Response
:
"""
Gibt eine ERROR-Antwort zurück, **ohne** dass Umlaute als
\\
uXXXX
escaped werden (ensure_ascii=False).
"""
payload
=
json
.
dumps
({
"status"
:
"ERROR"
,
"message"
:
message
},
ensure_ascii
=
False
)
payload
=
json
.
dumps
({
"status"
:
"ERROR"
,
"message"
:
message
},
ensure_ascii
=
False
)
return
Response
(
payload
,
status
=
status_code
,
mimetype
=
"application/json"
)
return
Response
(
payload
,
status
=
status_code
,
mimetype
=
"application/json"
)
def
_extract_options
()
->
List
[
str
]:
def
_extract_options
()
->
List
[
str
]:
"""
Wandelt Query-Parameter »options« in eine Liste um.
Akzeptiert:
?options=A,B,C
?options=A&options=B…
"""
raw
=
request
.
args
.
getlist
(
"options"
)
raw
=
request
.
args
.
getlist
(
"options"
)
if
len
(
raw
)
==
1
and
","
in
raw
[
0
]:
if
len
(
raw
)
==
1
and
","
in
raw
[
0
]:
return
[
opt
.
strip
()
for
opt
in
raw
[
0
]
.
split
(
","
)
if
opt
.
strip
()]
return
[
opt
.
strip
()
for
opt
in
raw
[
0
]
.
split
(
","
)
if
opt
.
strip
()]
...
@@ -98,14 +71,10 @@ def _extract_options() -> List[str]:
...
@@ -98,14 +71,10 @@ def _extract_options() -> List[str]:
def
_hash_id_options
(
tarif_id
:
str
,
options
:
List
[
str
])
->
str
:
def
_hash_id_options
(
tarif_id
:
str
,
options
:
List
[
str
])
->
str
:
key
=
f
"{tarif_id}:{','.join(sorted(options))}"
.
encode
(
"utf-8"
)
key
=
f
"{tarif_id}:{','.join(sorted(options))}
:{int(time.time())}
"
.
encode
(
"utf-8"
)
return
hashlib
.
sha256
(
key
)
.
hexdigest
()
return
hashlib
.
sha256
(
key
)
.
hexdigest
()
def
_ensure_cache
():
os
.
makedirs
(
CACHE_DIR
,
exist_ok
=
True
)
# ---------------------------- OAuth-Token ---------------------------------- #
# ---------------------------- OAuth-Token ---------------------------------- #
def
_get_token
()
->
Tuple
[
str
|
None
,
str
|
None
]:
def
_get_token
()
->
Tuple
[
str
|
None
,
str
|
None
]:
payload
=
{
payload
=
{
...
@@ -133,41 +102,99 @@ def _partner_api(token: str, tarif_id: str, options: List[str]) -> Tuple[dict |
...
@@ -133,41 +102,99 @@ def _partner_api(token: str, tarif_id: str, options: List[str]) -> Tuple[dict |
"CF-Access-Client-Secret"
:
CF_CLIENT_SECRET
,
"CF-Access-Client-Secret"
:
CF_CLIENT_SECRET
,
"Content-Type"
:
"application/json"
,
"Content-Type"
:
"application/json"
,
}
}
try
:
session
=
MysqlManager
()
.
getSession
()
deal_int
=
int
(
tarif_id
)
deal
=
(
session
.
query
(
DealDeal
)
.
options
(
joinedload
(
DealDeal
.
base
))
.
filter
(
DealDeal
.
id_deal
==
deal_int
)
.
one_or_none
()
)
if
deal
is
None
:
return
None
,
f
"Deal {tarif_id} not found."
base_obj
:
BaseBase
=
deal
.
base
if
not
base_obj
or
not
base_obj
.
providercode_base
:
return
None
,
f
"Kein providercode_base für Deal id={tarif_id} gefunden."
providercode_base_value
=
base_obj
.
providercode_base
providercode_deal_value
=
deal
.
providercode_deal
finally
:
session
.
close
()
am_aktion_id
:
int
|
None
=
None
if
providercode_deal_value
:
digits
=
""
.
join
(
filter
(
str
.
isdigit
,
providercode_deal_value
))
if
digits
:
am_aktion_id
=
int
(
digits
)
service_codes
:
List
[
str
]
=
[]
try
:
session
=
MysqlManager
()
.
getSession
()
for
opt_id
in
options
:
try
:
opt_int
=
int
(
opt_id
)
except
ValueError
:
continue
opt
=
(
session
.
query
(
OptionOpti
)
.
filter
(
OptionOpti
.
id_opti
==
opt_int
)
.
one_or_none
()
)
if
not
opt
or
not
opt
.
providercode_opti
:
continue
service_codes
.
append
(
opt
.
providercode_opti
)
parent_code
=
opt
.
providercategory_opti
while
parent_code
:
parent_opt
=
(
session
.
query
(
OptionOpti
)
.
filter
(
OptionOpti
.
providercode_opti
==
parent_code
)
.
limit
(
1
)
.
one_or_none
()
)
if
not
parent_opt
or
not
parent_opt
.
providercode_opti
:
break
service_codes
.
append
(
parent_opt
.
providercode_opti
)
parent_code
=
parent_opt
.
providercategory_opti
finally
:
session
.
close
()
payload
=
{
payload
=
{
"hdl_nr"
:
HDL_NR
,
"hdl_nr"
:
HDL_NR
,
"prov_hdl_nr"
:
PROV_HDL_NR
,
"prov_hdl_nr"
:
PROV_HDL_NR
,
"tarif_id"
:
tarif_id
,
"tarif_id"
:
providercode_base_value
,
"produkt_kategorie"
:
PRODUKT_KATEGORIE
,
"produkt_kategorie"
:
PRODUKT_KATEGORIE
,
"service_code"
:
option
s
,
"service_code"
:
service_code
s
,
}
}
if
am_aktion_id
is
not
None
:
payload
[
"am_aktion_id"
]
=
am_aktion_id
try
:
try
:
r
=
requests
.
put
(
API_URL
,
headers
=
headers
,
json
=
payload
,
verify
=
False
,
timeout
=
30
)
r
=
requests
.
put
(
API_URL
,
headers
=
headers
,
json
=
payload
,
verify
=
False
,
timeout
=
30
)
r
.
raise_for_status
()
r
.
raise_for_status
()
except
requests
.
exceptions
.
RequestException
as
exc
:
except
requests
.
exceptions
.
RequestException
as
exc
:
return
None
,
f
"API-Aufruf fehlgeschlagen: {exc}"
return
None
,
f
"API-Aufruf fehlgeschlagen: {exc}
– Payload: {payload}
"
try
:
try
:
data
=
r
.
json
()
data
=
r
.
json
()
except
ValueError
:
except
ValueError
:
return
None
,
"Antwort der Partner-API ist kein JSON."
return
None
,
"Antwort der Partner-API ist kein JSON."
# → Fehlerrückgabe der Partner-API auswerten
err_val
=
data
.
get
(
"error"
)
err_val
=
data
.
get
(
"error"
)
if
err_val
:
if
err_val
:
# Fehler kann Array, Dict oder JSON-String sein
if
isinstance
(
err_val
,
list
):
if
isinstance
(
err_val
,
list
):
msg
=
"; "
.
join
(
str
(
e
)
for
e
in
err_val
)
msg
=
"; "
.
join
(
str
(
e
)
for
e
in
err_val
)
elif
isinstance
(
err_val
,
dict
):
elif
isinstance
(
err_val
,
dict
):
msg
=
"; "
.
join
(
f
"{k}: {v}"
for
k
,
v
in
err_val
.
items
())
msg
=
"; "
.
join
(
f
"{k}: {v}"
for
k
,
v
in
err_val
.
items
())
elif
isinstance
(
err_val
,
str
):
elif
isinstance
(
err_val
,
str
):
# eventuell weitere JSON-Ebene
try
:
try
:
decoded
=
json
.
loads
(
err_val
)
decoded
=
json
.
loads
(
err_val
)
if
isinstance
(
decoded
,
dict
):
msg
=
decoded
.
get
(
"message"
,
str
(
decoded
))
if
isinstance
(
decoded
,
dict
)
else
str
(
decoded
)
msg
=
decoded
.
get
(
"message"
,
str
(
decoded
))
else
:
msg
=
str
(
decoded
)
except
ValueError
:
except
ValueError
:
msg
=
err_val
msg
=
err_val
else
:
else
:
...
@@ -194,51 +221,45 @@ def _download_pdf(url: str) -> Tuple[bytes | None, str | None]:
...
@@ -194,51 +221,45 @@ def _download_pdf(url: str) -> Tuple[bytes | None, str | None]:
def
eeccx_pdf
(
tarif_id
:
str
):
def
eeccx_pdf
(
tarif_id
:
str
):
"""
"""
Beispiel:
Beispiel:
/eeccx/3877325?options=G343,O3729
/
freenet-
eeccx/3877325?options=G343,O3729
/
eeccx/3877325?options=G343&options=O3729
/
freenet-eeccx/3877325?options=G343&options=B…
"""
"""
options
=
_extract_options
()
options
=
_extract_options
()
_ensure_cache
()
cache_file
=
os
.
path
.
join
(
CACHE_DIR
,
f
"{_hash_id_options(tarif_id, options)}.pdf"
)
# 1) OAuth-Token
# 1) Cache-Treffer → PDF sofort
if
os
.
path
.
isfile
(
cache_file
):
return
send_file
(
cache_file
,
mimetype
=
"application/pdf"
)
# 2) Token
token
,
err
=
_get_token
()
token
,
err
=
_get_token
()
if
err
:
if
err
:
return
_json_error
(
err
,
502
)
return
_json_error
(
err
,
502
)
#
3
) Partner-API
#
2
) Partner-API
api_json
,
err
=
_partner_api
(
token
,
tarif_id
,
options
)
api_json
,
err
=
_partner_api
(
token
,
tarif_id
,
options
)
if
err
:
if
err
:
return
_json_error
(
err
,
502
)
return
_json_error
(
err
,
502
)
#
4) PDF-URL
#
3) PDF-URL extrahieren
pdf_url
:
str
|
None
=
api_json
.
get
(
"pcsPdf"
)
or
api_json
.
get
(
"pciPdf"
)
pdf_url
=
api_json
.
get
(
"pcsPdf"
)
or
api_json
.
get
(
"pciPdf"
)
if
not
pdf_url
:
if
not
pdf_url
:
# falls API eine Message liefert, diese übernehmen
msg
=
api_json
.
get
(
"message"
)
or
"Keine PDF-URL in der API-Antwort."
msg
=
api_json
.
get
(
"message"
)
or
"Keine PDF-URL in der API-Antwort."
return
_json_error
(
msg
,
502
)
return
_json_error
(
msg
,
502
)
#
5
) PDF laden
#
4
) PDF laden
pdf_bytes
,
err
=
_download_pdf
(
pdf_url
)
pdf_bytes
,
err
=
_download_pdf
(
pdf_url
)
if
err
:
if
err
:
return
_json_error
(
err
,
502
)
return
_json_error
(
err
,
502
)
# 6) Cache speichern (Fehler ≠ KO)
# 5) Temporäre Datei zum Upload schreiben
try
:
hash_name
=
_hash_id_options
(
tarif_id
,
options
)
with
open
(
cache_file
,
"wb"
)
as
fh
:
with
tempfile
.
NamedTemporaryFile
(
delete
=
False
,
suffix
=
".pdf"
)
as
tmp
:
fh
.
write
(
pdf_bytes
)
tmp
.
write
(
pdf_bytes
)
except
OSError
:
tmp_path
=
tmp
.
name
pass
# Ignorieren, PDF wird dennoch geliefert
# 6) Upload zu S3
# 7) PDF senden
s3_key
=
f
"eeccx/{hash_name}.pdf"
return
send_file
(
url
=
s3_manager
.
uploadFile
(
tmp_path
,
s3_key
)
io
.
BytesIO
(
pdf_bytes
),
os
.
remove
(
tmp_path
)
mimetype
=
"application/pdf"
,
if
not
url
:
as_attachment
=
False
,
return
_json_error
(
f
"Upload zu S3 fehlgeschlagen für key={s3_key}"
,
502
)
download_name
=
f
"{tarif_id}.pdf"
,
)
# 7) Download-URL als JSON zurückgeben
payload
=
json
.
dumps
({
"url"
:
url
},
ensure_ascii
=
False
)
return
Response
(
payload
,
status
=
200
,
mimetype
=
"application/json"
)
routes/HealtCheckRouter.py
View file @
fc69800a
...
@@ -15,4 +15,4 @@ def index():
...
@@ -15,4 +15,4 @@ def index():
GET /
GET /
Liefert einen einfachen JSON-Status.
Liefert einen einfachen JSON-Status.
"""
"""
return
jsonify
({
"
status"
:
"ok"
,
"
message"
:
"The API is working."
})
return
jsonify
({
"message"
:
"The API is working."
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment