Repository: electricitymaps/electricitymaps-contrib

Issue:
GB-ORK production parser down
## Description

This is an automatic error report generated for Great Britain Orkney Islands (GB-ORK).

Issues:
- No recent data found for `production` parser

## Suggestions
- Try running the parser locally using the command `poetry run test_parser GB-ORK production`
- <a href="https://storage.googleapis.com/electricitymap-parser-logs/GB-ORK.html">Explore the runtime logs</a>

You can see an overview of all parser issues [here](https://github.com/tmrowco/electricitymap-contrib/wiki/Parser-issues).



Discussion:

Resolving patch:
diff --git a/parsers/GB_ORK.py b/parsers/GB_ORK.py
--- a/parsers/GB_ORK.py
+++ b/parsers/GB_ORK.py
@@ -12,6 +12,8 @@
 from bs4 import BeautifulSoup
 from requests import Response, Session
 
+from parsers.lib.session import get_session_with_legacy_adapter
+
 # There is a 2MW storage battery on the islands.
 # http://www.oref.co.uk/orkneys-energy/innovations-2/
 
@@ -26,13 +28,12 @@
 }
 
 
-def get_json_data(session):
+def get_json_data():
     """
     Requests json data and extracts generation information.
     Returns a dictionary.
     """
-    s = session or Session()
-    req = s.get(GENERATION_LINK)
+    req = get_session_with_legacy_adapter().get(GENERATION_LINK)
     raw_json_data = req.json()
 
     generation_data = raw_json_data["data"]["datasets"]
@@ -51,13 +52,12 @@ def get_json_data(session):
     return production
 
 
-def get_datetime(session):
+def get_datetime():
     """
     Extracts data timestamp from html and checks it's less than 2 hours old.
     Returns an arrow object.
     """
-    s = session or Session()
-    req: Response = s.get(DATETIME_LINK)
+    req: Response = get_session_with_legacy_adapter().get(DATETIME_LINK)
     soup = BeautifulSoup(req.text, "html.parser")
 
     data_table = soup.find("div", {"class": "Widget-Base Widget-ANMGraph"})
@@ -90,7 +90,7 @@ def fetch_production(
     if target_datetime:
         raise NotImplementedError("This parser is not yet able to parse past dates")
 
-    raw_data = get_json_data(session)
+    raw_data = get_json_data()
     raw_data.pop("Live Demand")
 
     mapped_data = {}
@@ -98,7 +98,7 @@ def fetch_production(
         "Non-ANM Renewable Generation", 0.0
     )
 
-    dt = get_datetime(session)
+    dt = get_datetime()
 
     data = {
         "zoneKey": zone_key,
@@ -122,8 +122,8 @@ def fetch_exchange(
 ) -> dict:
     """Requests the last known power exchange (in MW) between two zones."""
     sorted_zone_keys = "->".join(sorted([zone_key1, zone_key2]))
-    raw_data = get_json_data(session)
-    dt = get_datetime(session)
+    raw_data = get_json_data()
+    dt = get_datetime()
 
     # +ve importing from mainland
     # -ve export to mainland
diff --git a/parsers/TR.py b/parsers/TR.py
--- a/parsers/TR.py
+++ b/parsers/TR.py
@@ -9,6 +9,7 @@
 
 from parsers.lib.config import refetch_frequency
 from parsers.lib.exceptions import ParserException
+from parsers.lib.session import get_session_with_legacy_adapter
 from parsers.lib.validation import validate
 
 TR_TZ = pytz.timezone("Europe/Istanbul")
@@ -37,13 +38,13 @@
 }
 
 
-def fetch_data(session: Session, target_datetime: datetime, kind: str) -> dict:
+def fetch_data(target_datetime: datetime, kind: str) -> dict:
     url = "/".join((EPIAS_MAIN_URL, KINDS_MAPPING[kind]["url"]))
     params = {
         "startDate": (target_datetime).strftime("%Y-%m-%d"),
         "endDate": (target_datetime + timedelta(days=1)).strftime("%Y-%m-%d"),
     }
-    r: Response = session.get(url=url, params=params)
+    r: Response = get_session_with_legacy_adapter().get(url=url, params=params)
     if r.status_code == 200:
         return r.json()["body"][KINDS_MAPPING[kind]["json_key"]]
     else:
@@ -74,7 +75,7 @@ def validate_production_data(
 @refetch_frequency(timedelta(days=1))
 def fetch_production(
     zone_key: str = "TR",
-    session: Session = Session(),
+    session: Optional[Session] = None,
     target_datetime: Optional[datetime] = None,
     logger: Logger = getLogger(__name__),
 ) -> list:
@@ -84,9 +85,7 @@ def fetch_production(
         target_datetime = datetime.now(tz=TR_TZ)
         exclude_last_data_point = True
 
-    data = fetch_data(
-        session=session, target_datetime=target_datetime, kind="production"
-    )
+    data = fetch_data(target_datetime=target_datetime, kind="production")
 
     all_data_points = []
     for item in data:
@@ -115,16 +114,14 @@ def fetch_production(
 @refetch_frequency(timedelta(days=1))
 def fetch_consumption(
     zone_key: str = "TR",
-    session: Session = Session(),
+    session: Optional[Session] = None,
     target_datetime: Optional[datetime] = None,
     logger: Logger = getLogger(__name__),
 ) -> list:
     if target_datetime is None:
         target_datetime = datetime.now(tz=TR_TZ) - timedelta(hours=2)
 
-    data = fetch_data(
-        session=session, target_datetime=target_datetime, kind="consumption"
-    )
+    data = fetch_data(target_datetime=target_datetime, kind="consumption")
 
     all_data_points = []
     for item in data:
@@ -142,14 +139,14 @@ def fetch_consumption(
 @refetch_frequency(timedelta(days=1))
 def fetch_price(
     zone_key: str = "TR",
-    session: Session = Session(),
+    session: Optional[Session] = None,
     target_datetime: Optional[datetime] = None,
     logger: Logger = getLogger(__name__),
 ) -> list:
     if target_datetime is None:
         target_datetime = datetime.now(tz=TR_TZ)
 
-    data = fetch_data(session=session, target_datetime=target_datetime, kind="price")
+    data = fetch_data(target_datetime=target_datetime, kind="price")
     all_data_points = []
     for item in data:
         data_point = {
diff --git a/parsers/lib/session.py b/parsers/lib/session.py
new file mode 100644
--- /dev/null
+++ b/parsers/lib/session.py
@@ -0,0 +1,22 @@
+import ssl
+
+import urllib3
+from requests import Session, adapters
+
+
+class LegacyHttpAdapter(adapters.HTTPAdapter):
+    def init_poolmanager(self, connections, maxsize, block=False):
+        ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+        ctx.options |= 0x4  # OP_LEGACY_SERVER_CONNECT
+        self.poolmanager = urllib3.poolmanager.PoolManager(
+            num_pools=connections, maxsize=maxsize, block=block, ssl_context=ctx
+        )
+
+
+# Use a LegacyHttpAdapter to avoid "unsafe legacy renegotiation disabled" error
+# Original code source: https://stackoverflow.com/questions/71603314/ssl-error-unsafe-legacy-renegotiation-disabled
+def get_session_with_legacy_adapter():
+    session = Session()
+    session.mount("https://", LegacyHttpAdapter())
+    session.mount("http://", LegacyHttpAdapter())
+    return session


Test patch: