We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Panama parser down due to website redesign
The [website](https://sitr.cnd.com.pa/m/pub/gen.html) used by PA.py has changed and the parser is failing.

At first glance it looks like they just put on a lick of paint, so changing a few CSS selectors should probably suffice to get it working again.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `parsers/PA.py`
Content:
```
1 #!/usr/bin/env python3
2 # coding=utf-8
3 
4 # The arrow library is used to handle datetimes
5 import arrow
6 # The request library is used to fetch content through HTTP
7 import requests
8 # The BeautifulSoup library is used to parse HTML
9 from bs4 import BeautifulSoup
10 
11 import logging
12 
13 
14 def fetch_production(zone_key='PA', session=None, target_datetime=None, logger: logging.Logger = logging.getLogger(__name__)):
15     """Requests the last known production mix (in MW) of a given country
16 
17     Arguments:
18     zone_key (optional) -- used in case a parser is able to fetch multiple countries
19     session (optional)      -- request session passed in order to re-use an existing session
20 
21     Return:
22     A dictionary in the form:
23     {
24       'zoneKey': 'FR',
25       'datetime': '2017-01-01T00:00:00Z',
26       'production': {
27           'biomass': 0.0,
28           'coal': 0.0,
29           'gas': 0.0,
30           'hydro': 0.0,
31           'nuclear': null,
32           'oil': 0.0,
33           'solar': 0.0,
34           'wind': 0.0,
35           'geothermal': 0.0,
36           'unknown': 0.0
37       },
38       'storage': {
39           'hydro': -10.0,
40       },
41       'source': 'mysource.com'
42     }
43     """
44     if target_datetime:
45         raise NotImplementedError('This parser is not yet able to parse past dates')
46 
47     #Fetch page and load into BeautifulSoup
48     r = session or requests.session()
49     url = 'http://sitr.cnd.com.pa/m/pub/gen.html'
50     response = r.get(url)
51     response.encoding = 'utf-8'
52     html_doc = response.text
53     soup = BeautifulSoup(html_doc, 'html.parser')
54 
55     #Parse production from pie chart
56     productions = soup.find('table', {'class': 'sitr-pie-layout'}).find_all('span')
57     map_generation = {
58       'Hídrica': 'hydro',
59       'Eólica': 'wind',
60       'Solar': 'solar',
61       'Biogas': 'biomass',
62       'Térmica': 'unknown'
63     }
64     data = {
65         'zoneKey': 'PA',
66         'production': {
67           #Setting default values here so we can do += when parsing the thermal generation breakdown
68           'biomass': 0.0,
69           'coal': 0.0,
70           'gas': 0.0,
71           'hydro': 0.0,
72           'nuclear': 0.0,
73           'oil': 0.0,
74           'solar': 0.0,
75           'wind': 0.0,
76           'geothermal': 0.0,
77           'unknown': 0.0,
78         },
79         'storage': {},
80         'source': 'https://www.cnd.com.pa/',
81     }
82     for prod in productions:
83         prod_data = prod.string.split(' ')
84         production_mean = map_generation[prod_data[0]]
85         production_value = float(prod_data[1])
86         data['production'][production_mean] = production_value
87 
88     #Known fossil plants: parse, subtract from "unknown", add to "coal"/"oil"/"gas"
89     thermal_production_breakdown = soup.find_all('table', {'class': 'sitr-table-gen'})[1]
90     #Make sure the table header is indeed "Térmicas (MW)" (in case the tables are re-arranged)
91     thermal_production_breakdown_table_header = thermal_production_breakdown.select('thead > tr > td > span')[0].string
92     assert ('Térmicas' in thermal_production_breakdown_table_header), (
93       "Exception when extracting thermal generation breakdown for {}: table header does not contain "
94       "'Térmicas' but is instead named {}".format(zone_key, thermal_production_breakdown_table_header)
95     )
96     thermal_production_units = thermal_production_breakdown.select('tbody tr td table.sitr-gen-group tr')
97     map_thermal_generation_unit_name_to_fuel_type = {
98       'ACP Miraflores 2': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
99       'ACP Miraflores 5': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
100       'ACP Miraflores 6': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
101       'ACP Miraflores 7': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
102       'ACP Miraflores 8': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
103       'ACP Miraflores 9': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
104       'ACP Miraflores 10': 'oil',#[7] Sheet "C-GE-1A-1 CapInstXEmp"
105       'BLM 2': 'coal',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
106       'BLM 3': 'coal',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
107       'BLM 4': 'coal',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
108       'BLM 5': 'oil',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
109       'BLM 6': 'oil',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
110       'BLM 8': 'oil',#[7] Sheet "C-GE-1A-2 CapInstXEmp"
111       'BLM 9': 'oil',#[7] Sheet "C-GE-1A-2 CapInstXEmp" mentions no fuel type, and given all other units are accounted for this must be the heat recovery boiler for the 3 diesel-fired units mentioned in [2]
112       'Cativá 1': 'oil',#[1][2]
113       'Cativá 2': 'oil',#[1][2]
114       'Cativá 3': 'oil',#[1][2]
115       'Cativá 4': 'oil',#[1][2]
116       'Cativá 5': 'oil',#[1][2]
117       'Cativá 6': 'oil',#[1][2]
118       'Cativá 7': 'oil',#[1][2]
119       'Cativá 8': 'oil',#[1][2]
120       'Cativá 9': 'oil',#[1][2]
121       'Cativá 10': 'oil',#[1][2]
122       'Cobre Panamá 1': 'coal',#[3]
123       'Cobre Panamá 2': 'coal',#[3]
124       'Costa Norte 1': 'gas',#[4][5]
125       'Costa Norte 2': 'gas',#[4][5]
126       'Costa Norte 3': 'gas',#[4][5]
127       'Costa Norte 4': 'gas',#[4][5]
128       'Esperanza 1': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
129       'Esperanza 2': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
130       'Esperanza 3': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
131       'Esperanza 4': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
132       'Esperanza 5': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
133       'Esperanza 6': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
134       'Esperanza 7': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units
135       'Jinro': 'oil',#[6][7]
136       'Pacora 1': 'oil',#[6]
137       'Pacora 2': 'oil',#[6]
138       'Pacora 3': 'oil',#[6]
139       'PanAm 1': 'oil',#[6][7]
140       'PanAm 2': 'oil',#[6][7]
141       'PanAm 3': 'oil',#[6][7]
142       'PanAm 4': 'oil',#[6][7]
143       'PanAm 5': 'oil',#[6][7]
144       'PanAm 6': 'oil',#[6][7]
145       'PanAm 7': 'oil',#[6][7]
146       'PanAm 8': 'oil',#[6][7]
147       'PanAm 9': 'oil',#[6][7]
148       'Termocolón 1': 'oil',#[6] (spelled "Termo Colón")
149       'Termocolón 2': 'oil',#[6] (spelled "Termo Colón")
150       'Termocolón 3': 'oil',#[6] (spelled "Termo Colón")
151       'Tropitérmica 1': 'oil',#[6]:162[7] spelled "Tropitermica" in both
152       'Tropitérmica 2': 'oil',#[6]:162[7] spelled "Tropitermica" in both
153       'Tropitérmica 3': 'oil',#[6]:162[7] spelled "Tropitermica" in both
154     }
155     #Sources:
156     #1. https://www.celsia.com/Portals/0/contenidos-celsia/accionistas-e-inversionistas/perfil-corporativo-US/presentaciones-US/2014/presentacion-morgan-ingles-v2.pdf
157     #2. https://www.celsia.com/en/about-celsia/business-model/power-generation/thermoelectric-power-plants
158     #3. https://endcoal.org/tracker/
159     #4. http://aesmcac.com/aespanamades/en/colon/ "It reuses the heat from the exhaust gas from the gas turbines in order to obtain steam, to be later used by a steam turbine and to save fuel consumption in the production of electricity."
160     #5. https://panamcham.com/sites/default/files/el_inicio_del_futuro_del_gas_natural_en_panama.pdf "3 gas turbines and 1 steam (3X1 configuration)" "Technology: Combined Cycle" | This and the previous source taken together seems to imply that the steam turbine is responsible for the second cycle of the CCGT plant, giving confidence that output from all four units should indeed be tallied under "gas". Furthermore, as the plant also has a LNG import facility it is most unlikely the steam turbine would be burning a different fuel such as coal or oil.
161     #6. https://www.etesa.com.pa/documentos/Tomo_II__Plan_Indicativo_de_Generacin_2019__2033.pdf page 142
162     #7. http://168.77.210.79/energia/wp-content/uploads/sites/2/2020/08/2-CEE-1970-2019-GE-Generaci%C3%B3n-El%C3%A9ctrica.xls (via http://www.energia.gob.pa/mercado-energetico/?tag=84#documents-list)
163     #8. https://www.asep.gob.pa/wp-content/uploads/electricidad/resoluciones/anno_12528_elec.pdf
164     for thermal_production_unit in thermal_production_units:
165       unit_name_and_generation = thermal_production_unit.find_all('td')
166       unit_name = unit_name_and_generation[0].string
167       unit_generation = float(unit_name_and_generation[1].string)
168       if(unit_name in map_thermal_generation_unit_name_to_fuel_type):
169         if(unit_generation > 0):#Ignore self-consumption
170           unit_fuel_type = map_thermal_generation_unit_name_to_fuel_type[unit_name]
171           data['production'][unit_fuel_type] += unit_generation
172           data['production']['unknown'] -= unit_generation
173       else:
174         logger.warning(u'{} is not mapped to generation type'.format(unit_name), extra={'key': zone_key})
175 
176     #Thermal total from the graph and the total one would get from summing output of all generators deviates a bit,
177     #presumably because they aren't updated at the exact same moment.
178     #Because negative production causes an error with ElectricityMap, we'll ignore small amounts of negative production
179     #TODO we might want to use the sum of the production of all thermal units instead of this workaround,
180     #because now we're still reporting small *postive* amounts of "ghost" thermal production
181     if data['production']['unknown'] < 0 and data['production']['unknown'] > -10:
182       logger.info(f"Ignoring small amount of negative thermal generation ({data['production']['unknown']}MW)", extra={"key": zone_key})
183       data['production']['unknown'] = 0
184 
185     #Round remaining "unknown" output to 13 decimal places to get rid of floating point errors
186     data['production']['unknown'] = round(data['production']['unknown'],13)
187 
188     # Parse the datetime and return a python datetime object
189     spanish_date = soup.find('div', {'class': 'sitr-update'}).find('span').string
190     date = arrow.get(spanish_date, 'DD-MMMM-YYYY H:mm:ss', locale="es", tzinfo="America/Panama")
191     data['datetime'] = date.datetime
192 
193     return data
194 
195 
196 if __name__ == '__main__':
197     """Main method, never used by the Electricity Map backend, but handy for testing."""
198 
199     print('fetch_production() ->')
200     print(fetch_production())
201
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/parsers/PA.py b/parsers/PA.py
--- a/parsers/PA.py
+++ b/parsers/PA.py
@@ -7,10 +7,20 @@
 import requests
 # The BeautifulSoup library is used to parse HTML
 from bs4 import BeautifulSoup
+# Extracting some data using regex
+import re
+import json
 
 import logging
 
 
+def extract_pie_chart_data(html):
+  """Extracts generation breakdown pie chart data from the source code of the page"""
+  dataSource = re.search(r"var localPie = (\[\{.+\}\]);", html).group(1)#Extract object with data
+  dataSource = re.sub(r"(name|value|color)", r'"\1"', dataSource) #Un-quoted keys ({key:"value"}) are valid JavaScript but not valid JSON (which requires {"key":"value"}). Will break if other keys than these three are introduced. Alternatively, use a JSON5 library (JSON5 allows un-quoted keys)
+  return json.loads(dataSource)
+
+
 def fetch_production(zone_key='PA', session=None, target_datetime=None, logger: logging.Logger = logging.getLogger(__name__)):
     """Requests the last known production mix (in MW) of a given country
 
@@ -53,7 +63,7 @@
     soup = BeautifulSoup(html_doc, 'html.parser')
 
     #Parse production from pie chart
-    productions = soup.find('table', {'class': 'sitr-pie-layout'}).find_all('span')
+    productions = extract_pie_chart_data(html_doc) #[{name:"Hídrica 1342.54 (80.14%)",value:1342.54,color:"#99ccee"}, ...]
     map_generation = {
       'Hídrica': 'hydro',
       'Eólica': 'wind',
@@ -79,16 +89,16 @@
         'storage': {},
         'source': 'https://www.cnd.com.pa/',
     }
-    for prod in productions:
-        prod_data = prod.string.split(' ')
-        production_mean = map_generation[prod_data[0]]
-        production_value = float(prod_data[1])
-        data['production'][production_mean] = production_value
+    for prod in productions: #{name:"Hídrica 1342.54 (80.14%)", ...}
+        prod_data = prod['name'].split(' ') #"Hídrica 1342.54 (80.14%)"
+        production_type = map_generation[prod_data[0]] #Hídrica
+        production_value = float(prod_data[1]) #1342.54
+        data['production'][production_type] = production_value
 
     #Known fossil plants: parse, subtract from "unknown", add to "coal"/"oil"/"gas"
     thermal_production_breakdown = soup.find_all('table', {'class': 'sitr-table-gen'})[1]
     #Make sure the table header is indeed "Térmicas (MW)" (in case the tables are re-arranged)
-    thermal_production_breakdown_table_header = thermal_production_breakdown.select('thead > tr > td > span')[0].string
+    thermal_production_breakdown_table_header = thermal_production_breakdown.parent.parent.parent.select('> .tile-title')[0].string
     assert ('Térmicas' in thermal_production_breakdown_table_header), (
       "Exception when extracting thermal generation breakdown for {}: table header does not contain "
       "'Térmicas' but is instead named {}".format(zone_key, thermal_production_breakdown_table_header)
@@ -186,7 +196,7 @@
     data['production']['unknown'] = round(data['production']['unknown'],13)
 
     # Parse the datetime and return a python datetime object
-    spanish_date = soup.find('div', {'class': 'sitr-update'}).find('span').string
+    spanish_date = soup.find('h3', {'class': 'sitr-update'}).string
     date = arrow.get(spanish_date, 'DD-MMMM-YYYY H:mm:ss', locale="es", tzinfo="America/Panama")
     data['datetime'] = date.datetime

{"golden_diff": "diff --git a/parsers/PA.py b/parsers/PA.py\n--- a/parsers/PA.py\n+++ b/parsers/PA.py\n@@ -7,10 +7,20 @@\n import requests\n # The BeautifulSoup library is used to parse HTML\n from bs4 import BeautifulSoup\n+# Extracting some data using regex\n+import re\n+import json\n \n import logging\n \n \n+def extract_pie_chart_data(html):\n+  \"\"\"Extracts generation breakdown pie chart data from the source code of the page\"\"\"\n+  dataSource = re.search(r\"var localPie = (\\[\\{.+\\}\\]);\", html).group(1)#Extract object with data\n+  dataSource = re.sub(r\"(name|value|color)\", r'\"\\1\"', dataSource) #Un-quoted keys ({key:\"value\"}) are valid JavaScript but not valid JSON (which requires {\"key\":\"value\"}). Will break if other keys than these three are introduced. Alternatively, use a JSON5 library (JSON5 allows un-quoted keys)\n+  return json.loads(dataSource)\n+\n+\n def fetch_production(zone_key='PA', session=None, target_datetime=None, logger: logging.Logger = logging.getLogger(__name__)):\n     \"\"\"Requests the last known production mix (in MW) of a given country\n \n@@ -53,7 +63,7 @@\n     soup = BeautifulSoup(html_doc, 'html.parser')\n \n     #Parse production from pie chart\n-    productions = soup.find('table', {'class': 'sitr-pie-layout'}).find_all('span')\n+    productions = extract_pie_chart_data(html_doc) #[{name:\"H\u00eddrica 1342.54 (80.14%)\",value:1342.54,color:\"#99ccee\"}, ...]\n     map_generation = {\n       'H\u00eddrica': 'hydro',\n       'E\u00f3lica': 'wind',\n@@ -79,16 +89,16 @@\n         'storage': {},\n         'source': 'https://www.cnd.com.pa/',\n     }\n-    for prod in productions:\n-        prod_data = prod.string.split(' ')\n-        production_mean = map_generation[prod_data[0]]\n-        production_value = float(prod_data[1])\n-        data['production'][production_mean] = production_value\n+    for prod in productions: #{name:\"H\u00eddrica 1342.54 (80.14%)\", ...}\n+        prod_data = prod['name'].split(' ') #\"H\u00eddrica 1342.54 (80.14%)\"\n+        production_type = map_generation[prod_data[0]] #H\u00eddrica\n+        production_value = float(prod_data[1]) #1342.54\n+        data['production'][production_type] = production_value\n \n     #Known fossil plants: parse, subtract from \"unknown\", add to \"coal\"/\"oil\"/\"gas\"\n     thermal_production_breakdown = soup.find_all('table', {'class': 'sitr-table-gen'})[1]\n     #Make sure the table header is indeed \"T\u00e9rmicas (MW)\" (in case the tables are re-arranged)\n-    thermal_production_breakdown_table_header = thermal_production_breakdown.select('thead > tr > td > span')[0].string\n+    thermal_production_breakdown_table_header = thermal_production_breakdown.parent.parent.parent.select('> .tile-title')[0].string\n     assert ('T\u00e9rmicas' in thermal_production_breakdown_table_header), (\n       \"Exception when extracting thermal generation breakdown for {}: table header does not contain \"\n       \"'T\u00e9rmicas' but is instead named {}\".format(zone_key, thermal_production_breakdown_table_header)\n@@ -186,7 +196,7 @@\n     data['production']['unknown'] = round(data['production']['unknown'],13)\n \n     # Parse the datetime and return a python datetime object\n-    spanish_date = soup.find('div', {'class': 'sitr-update'}).find('span').string\n+    spanish_date = soup.find('h3', {'class': 'sitr-update'}).string\n     date = arrow.get(spanish_date, 'DD-MMMM-YYYY H:mm:ss', locale=\"es\", tzinfo=\"America/Panama\")\n     data['datetime'] = date.datetime\n", "issue": "Panama parser down due to website redesign\nThe [website](https://sitr.cnd.com.pa/m/pub/gen.html) used by PA.py has changed and the parser is failing.\r\n\r\nAt first glance it looks like they just put on a lick of paint, so changing a few CSS selectors should probably suffice to get it working again.\n", "before_files": [{"content": "#!/usr/bin/env python3\n# coding=utf-8\n\n# The arrow library is used to handle datetimes\nimport arrow\n# The request library is used to fetch content through HTTP\nimport requests\n# The BeautifulSoup library is used to parse HTML\nfrom bs4 import BeautifulSoup\n\nimport logging\n\n\ndef fetch_production(zone_key='PA', session=None, target_datetime=None, logger: logging.Logger = logging.getLogger(__name__)):\n    \"\"\"Requests the last known production mix (in MW) of a given country\n\n    Arguments:\n    zone_key (optional) -- used in case a parser is able to fetch multiple countries\n    session (optional)      -- request session passed in order to re-use an existing session\n\n    Return:\n    A dictionary in the form:\n    {\n      'zoneKey': 'FR',\n      'datetime': '2017-01-01T00:00:00Z',\n      'production': {\n          'biomass': 0.0,\n          'coal': 0.0,\n          'gas': 0.0,\n          'hydro': 0.0,\n          'nuclear': null,\n          'oil': 0.0,\n          'solar': 0.0,\n          'wind': 0.0,\n          'geothermal': 0.0,\n          'unknown': 0.0\n      },\n      'storage': {\n          'hydro': -10.0,\n      },\n      'source': 'mysource.com'\n    }\n    \"\"\"\n    if target_datetime:\n        raise NotImplementedError('This parser is not yet able to parse past dates')\n\n    #Fetch page and load into BeautifulSoup\n    r = session or requests.session()\n    url = 'http://sitr.cnd.com.pa/m/pub/gen.html'\n    response = r.get(url)\n    response.encoding = 'utf-8'\n    html_doc = response.text\n    soup = BeautifulSoup(html_doc, 'html.parser')\n\n    #Parse production from pie chart\n    productions = soup.find('table', {'class': 'sitr-pie-layout'}).find_all('span')\n    map_generation = {\n      'H\u00eddrica': 'hydro',\n      'E\u00f3lica': 'wind',\n      'Solar': 'solar',\n      'Biogas': 'biomass',\n      'T\u00e9rmica': 'unknown'\n    }\n    data = {\n        'zoneKey': 'PA',\n        'production': {\n          #Setting default values here so we can do += when parsing the thermal generation breakdown\n          'biomass': 0.0,\n          'coal': 0.0,\n          'gas': 0.0,\n          'hydro': 0.0,\n          'nuclear': 0.0,\n          'oil': 0.0,\n          'solar': 0.0,\n          'wind': 0.0,\n          'geothermal': 0.0,\n          'unknown': 0.0,\n        },\n        'storage': {},\n        'source': 'https://www.cnd.com.pa/',\n    }\n    for prod in productions:\n        prod_data = prod.string.split(' ')\n        production_mean = map_generation[prod_data[0]]\n        production_value = float(prod_data[1])\n        data['production'][production_mean] = production_value\n\n    #Known fossil plants: parse, subtract from \"unknown\", add to \"coal\"/\"oil\"/\"gas\"\n    thermal_production_breakdown = soup.find_all('table', {'class': 'sitr-table-gen'})[1]\n    #Make sure the table header is indeed \"T\u00e9rmicas (MW)\" (in case the tables are re-arranged)\n    thermal_production_breakdown_table_header = thermal_production_breakdown.select('thead > tr > td > span')[0].string\n    assert ('T\u00e9rmicas' in thermal_production_breakdown_table_header), (\n      \"Exception when extracting thermal generation breakdown for {}: table header does not contain \"\n      \"'T\u00e9rmicas' but is instead named {}\".format(zone_key, thermal_production_breakdown_table_header)\n    )\n    thermal_production_units = thermal_production_breakdown.select('tbody tr td table.sitr-gen-group tr')\n    map_thermal_generation_unit_name_to_fuel_type = {\n      'ACP Miraflores 2': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 5': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 6': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 7': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 8': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 9': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 10': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'BLM 2': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 3': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 4': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 5': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 6': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 8': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 9': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\" mentions no fuel type, and given all other units are accounted for this must be the heat recovery boiler for the 3 diesel-fired units mentioned in [2]\n      'Cativ\u00e1 1': 'oil',#[1][2]\n      'Cativ\u00e1 2': 'oil',#[1][2]\n      'Cativ\u00e1 3': 'oil',#[1][2]\n      'Cativ\u00e1 4': 'oil',#[1][2]\n      'Cativ\u00e1 5': 'oil',#[1][2]\n      'Cativ\u00e1 6': 'oil',#[1][2]\n      'Cativ\u00e1 7': 'oil',#[1][2]\n      'Cativ\u00e1 8': 'oil',#[1][2]\n      'Cativ\u00e1 9': 'oil',#[1][2]\n      'Cativ\u00e1 10': 'oil',#[1][2]\n      'Cobre Panam\u00e1 1': 'coal',#[3]\n      'Cobre Panam\u00e1 2': 'coal',#[3]\n      'Costa Norte 1': 'gas',#[4][5]\n      'Costa Norte 2': 'gas',#[4][5]\n      'Costa Norte 3': 'gas',#[4][5]\n      'Costa Norte 4': 'gas',#[4][5]\n      'Esperanza 1': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 2': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 3': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 4': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 5': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 6': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 7': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Jinro': 'oil',#[6][7]\n      'Pacora 1': 'oil',#[6]\n      'Pacora 2': 'oil',#[6]\n      'Pacora 3': 'oil',#[6]\n      'PanAm 1': 'oil',#[6][7]\n      'PanAm 2': 'oil',#[6][7]\n      'PanAm 3': 'oil',#[6][7]\n      'PanAm 4': 'oil',#[6][7]\n      'PanAm 5': 'oil',#[6][7]\n      'PanAm 6': 'oil',#[6][7]\n      'PanAm 7': 'oil',#[6][7]\n      'PanAm 8': 'oil',#[6][7]\n      'PanAm 9': 'oil',#[6][7]\n      'Termocol\u00f3n 1': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Termocol\u00f3n 2': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Termocol\u00f3n 3': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Tropit\u00e9rmica 1': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n      'Tropit\u00e9rmica 2': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n      'Tropit\u00e9rmica 3': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n    }\n    #Sources:\n    #1. https://www.celsia.com/Portals/0/contenidos-celsia/accionistas-e-inversionistas/perfil-corporativo-US/presentaciones-US/2014/presentacion-morgan-ingles-v2.pdf\n    #2. https://www.celsia.com/en/about-celsia/business-model/power-generation/thermoelectric-power-plants\n    #3. https://endcoal.org/tracker/\n    #4. http://aesmcac.com/aespanamades/en/colon/ \"It reuses the heat from the exhaust gas from the gas turbines in order to obtain steam, to be later used by a steam turbine and to save fuel consumption in the production of electricity.\"\n    #5. https://panamcham.com/sites/default/files/el_inicio_del_futuro_del_gas_natural_en_panama.pdf \"3 gas turbines and 1 steam (3X1 configuration)\" \"Technology: Combined Cycle\" | This and the previous source taken together seems to imply that the steam turbine is responsible for the second cycle of the CCGT plant, giving confidence that output from all four units should indeed be tallied under \"gas\". Furthermore, as the plant also has a LNG import facility it is most unlikely the steam turbine would be burning a different fuel such as coal or oil.\n    #6. https://www.etesa.com.pa/documentos/Tomo_II__Plan_Indicativo_de_Generacin_2019__2033.pdf page 142\n    #7. http://168.77.210.79/energia/wp-content/uploads/sites/2/2020/08/2-CEE-1970-2019-GE-Generaci%C3%B3n-El%C3%A9ctrica.xls (via http://www.energia.gob.pa/mercado-energetico/?tag=84#documents-list)\n    #8. https://www.asep.gob.pa/wp-content/uploads/electricidad/resoluciones/anno_12528_elec.pdf\n    for thermal_production_unit in thermal_production_units:\n      unit_name_and_generation = thermal_production_unit.find_all('td')\n      unit_name = unit_name_and_generation[0].string\n      unit_generation = float(unit_name_and_generation[1].string)\n      if(unit_name in map_thermal_generation_unit_name_to_fuel_type):\n        if(unit_generation > 0):#Ignore self-consumption\n          unit_fuel_type = map_thermal_generation_unit_name_to_fuel_type[unit_name]\n          data['production'][unit_fuel_type] += unit_generation\n          data['production']['unknown'] -= unit_generation\n      else:\n        logger.warning(u'{} is not mapped to generation type'.format(unit_name), extra={'key': zone_key})\n\n    #Thermal total from the graph and the total one would get from summing output of all generators deviates a bit,\n    #presumably because they aren't updated at the exact same moment.\n    #Because negative production causes an error with ElectricityMap, we'll ignore small amounts of negative production\n    #TODO we might want to use the sum of the production of all thermal units instead of this workaround,\n    #because now we're still reporting small *postive* amounts of \"ghost\" thermal production\n    if data['production']['unknown'] < 0 and data['production']['unknown'] > -10:\n      logger.info(f\"Ignoring small amount of negative thermal generation ({data['production']['unknown']}MW)\", extra={\"key\": zone_key})\n      data['production']['unknown'] = 0\n\n    #Round remaining \"unknown\" output to 13 decimal places to get rid of floating point errors\n    data['production']['unknown'] = round(data['production']['unknown'],13)\n\n    # Parse the datetime and return a python datetime object\n    spanish_date = soup.find('div', {'class': 'sitr-update'}).find('span').string\n    date = arrow.get(spanish_date, 'DD-MMMM-YYYY H:mm:ss', locale=\"es\", tzinfo=\"America/Panama\")\n    data['datetime'] = date.datetime\n\n    return data\n\n\nif __name__ == '__main__':\n    \"\"\"Main method, never used by the Electricity Map backend, but handy for testing.\"\"\"\n\n    print('fetch_production() ->')\n    print(fetch_production())\n", "path": "parsers/PA.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n# coding=utf-8\n\n# The arrow library is used to handle datetimes\nimport arrow\n# The request library is used to fetch content through HTTP\nimport requests\n# The BeautifulSoup library is used to parse HTML\nfrom bs4 import BeautifulSoup\n# Extracting some data using regex\nimport re\nimport json\n\nimport logging\n\n\ndef extract_pie_chart_data(html):\n  \"\"\"Extracts generation breakdown pie chart data from the source code of the page\"\"\"\n  dataSource = re.search(r\"var localPie = (\\[\\{.+\\}\\]);\", html).group(1)#Extract object with data\n  dataSource = re.sub(r\"(name|value|color)\", r'\"\\1\"', dataSource) #Un-quoted keys ({key:\"value\"}) are valid JavaScript but not valid JSON (which requires {\"key\":\"value\"}). Will break if other keys than these three are introduced. Alternatively, use a JSON5 library (JSON5 allows un-quoted keys)\n  return json.loads(dataSource)\n\n\ndef fetch_production(zone_key='PA', session=None, target_datetime=None, logger: logging.Logger = logging.getLogger(__name__)):\n    \"\"\"Requests the last known production mix (in MW) of a given country\n\n    Arguments:\n    zone_key (optional) -- used in case a parser is able to fetch multiple countries\n    session (optional)      -- request session passed in order to re-use an existing session\n\n    Return:\n    A dictionary in the form:\n    {\n      'zoneKey': 'FR',\n      'datetime': '2017-01-01T00:00:00Z',\n      'production': {\n          'biomass': 0.0,\n          'coal': 0.0,\n          'gas': 0.0,\n          'hydro': 0.0,\n          'nuclear': null,\n          'oil': 0.0,\n          'solar': 0.0,\n          'wind': 0.0,\n          'geothermal': 0.0,\n          'unknown': 0.0\n      },\n      'storage': {\n          'hydro': -10.0,\n      },\n      'source': 'mysource.com'\n    }\n    \"\"\"\n    if target_datetime:\n        raise NotImplementedError('This parser is not yet able to parse past dates')\n\n    #Fetch page and load into BeautifulSoup\n    r = session or requests.session()\n    url = 'http://sitr.cnd.com.pa/m/pub/gen.html'\n    response = r.get(url)\n    response.encoding = 'utf-8'\n    html_doc = response.text\n    soup = BeautifulSoup(html_doc, 'html.parser')\n\n    #Parse production from pie chart\n    productions = extract_pie_chart_data(html_doc) #[{name:\"H\u00eddrica 1342.54 (80.14%)\",value:1342.54,color:\"#99ccee\"}, ...]\n    map_generation = {\n      'H\u00eddrica': 'hydro',\n      'E\u00f3lica': 'wind',\n      'Solar': 'solar',\n      'Biogas': 'biomass',\n      'T\u00e9rmica': 'unknown'\n    }\n    data = {\n        'zoneKey': 'PA',\n        'production': {\n          #Setting default values here so we can do += when parsing the thermal generation breakdown\n          'biomass': 0.0,\n          'coal': 0.0,\n          'gas': 0.0,\n          'hydro': 0.0,\n          'nuclear': 0.0,\n          'oil': 0.0,\n          'solar': 0.0,\n          'wind': 0.0,\n          'geothermal': 0.0,\n          'unknown': 0.0,\n        },\n        'storage': {},\n        'source': 'https://www.cnd.com.pa/',\n    }\n    for prod in productions: #{name:\"H\u00eddrica 1342.54 (80.14%)\", ...}\n        prod_data = prod['name'].split(' ') #\"H\u00eddrica 1342.54 (80.14%)\"\n        production_type = map_generation[prod_data[0]] #H\u00eddrica\n        production_value = float(prod_data[1]) #1342.54\n        data['production'][production_type] = production_value\n\n    #Known fossil plants: parse, subtract from \"unknown\", add to \"coal\"/\"oil\"/\"gas\"\n    thermal_production_breakdown = soup.find_all('table', {'class': 'sitr-table-gen'})[1]\n    #Make sure the table header is indeed \"T\u00e9rmicas (MW)\" (in case the tables are re-arranged)\n    thermal_production_breakdown_table_header = thermal_production_breakdown.parent.parent.parent.select('> .tile-title')[0].string\n    assert ('T\u00e9rmicas' in thermal_production_breakdown_table_header), (\n      \"Exception when extracting thermal generation breakdown for {}: table header does not contain \"\n      \"'T\u00e9rmicas' but is instead named {}\".format(zone_key, thermal_production_breakdown_table_header)\n    )\n    thermal_production_units = thermal_production_breakdown.select('tbody tr td table.sitr-gen-group tr')\n    map_thermal_generation_unit_name_to_fuel_type = {\n      'ACP Miraflores 2': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 5': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 6': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 7': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 8': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 9': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'ACP Miraflores 10': 'oil',#[7] Sheet \"C-GE-1A-1 CapInstXEmp\"\n      'BLM 2': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 3': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 4': 'coal',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 5': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 6': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 8': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\"\n      'BLM 9': 'oil',#[7] Sheet \"C-GE-1A-2 CapInstXEmp\" mentions no fuel type, and given all other units are accounted for this must be the heat recovery boiler for the 3 diesel-fired units mentioned in [2]\n      'Cativ\u00e1 1': 'oil',#[1][2]\n      'Cativ\u00e1 2': 'oil',#[1][2]\n      'Cativ\u00e1 3': 'oil',#[1][2]\n      'Cativ\u00e1 4': 'oil',#[1][2]\n      'Cativ\u00e1 5': 'oil',#[1][2]\n      'Cativ\u00e1 6': 'oil',#[1][2]\n      'Cativ\u00e1 7': 'oil',#[1][2]\n      'Cativ\u00e1 8': 'oil',#[1][2]\n      'Cativ\u00e1 9': 'oil',#[1][2]\n      'Cativ\u00e1 10': 'oil',#[1][2]\n      'Cobre Panam\u00e1 1': 'coal',#[3]\n      'Cobre Panam\u00e1 2': 'coal',#[3]\n      'Costa Norte 1': 'gas',#[4][5]\n      'Costa Norte 2': 'gas',#[4][5]\n      'Costa Norte 3': 'gas',#[4][5]\n      'Costa Norte 4': 'gas',#[4][5]\n      'Esperanza 1': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 2': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 3': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 4': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 5': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 6': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Esperanza 7': 'oil',#[7] has a single 92MW bunker fuel power plant, but [8] shows this is actually a power barge with 7 units\n      'Jinro': 'oil',#[6][7]\n      'Pacora 1': 'oil',#[6]\n      'Pacora 2': 'oil',#[6]\n      'Pacora 3': 'oil',#[6]\n      'PanAm 1': 'oil',#[6][7]\n      'PanAm 2': 'oil',#[6][7]\n      'PanAm 3': 'oil',#[6][7]\n      'PanAm 4': 'oil',#[6][7]\n      'PanAm 5': 'oil',#[6][7]\n      'PanAm 6': 'oil',#[6][7]\n      'PanAm 7': 'oil',#[6][7]\n      'PanAm 8': 'oil',#[6][7]\n      'PanAm 9': 'oil',#[6][7]\n      'Termocol\u00f3n 1': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Termocol\u00f3n 2': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Termocol\u00f3n 3': 'oil',#[6] (spelled \"Termo Col\u00f3n\")\n      'Tropit\u00e9rmica 1': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n      'Tropit\u00e9rmica 2': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n      'Tropit\u00e9rmica 3': 'oil',#[6]:162[7] spelled \"Tropitermica\" in both\n    }\n    #Sources:\n    #1. https://www.celsia.com/Portals/0/contenidos-celsia/accionistas-e-inversionistas/perfil-corporativo-US/presentaciones-US/2014/presentacion-morgan-ingles-v2.pdf\n    #2. https://www.celsia.com/en/about-celsia/business-model/power-generation/thermoelectric-power-plants\n    #3. https://endcoal.org/tracker/\n    #4. http://aesmcac.com/aespanamades/en/colon/ \"It reuses the heat from the exhaust gas from the gas turbines in order to obtain steam, to be later used by a steam turbine and to save fuel consumption in the production of electricity.\"\n    #5. https://panamcham.com/sites/default/files/el_inicio_del_futuro_del_gas_natural_en_panama.pdf \"3 gas turbines and 1 steam (3X1 configuration)\" \"Technology: Combined Cycle\" | This and the previous source taken together seems to imply that the steam turbine is responsible for the second cycle of the CCGT plant, giving confidence that output from all four units should indeed be tallied under \"gas\". Furthermore, as the plant also has a LNG import facility it is most unlikely the steam turbine would be burning a different fuel such as coal or oil.\n    #6. https://www.etesa.com.pa/documentos/Tomo_II__Plan_Indicativo_de_Generacin_2019__2033.pdf page 142\n    #7. http://168.77.210.79/energia/wp-content/uploads/sites/2/2020/08/2-CEE-1970-2019-GE-Generaci%C3%B3n-El%C3%A9ctrica.xls (via http://www.energia.gob.pa/mercado-energetico/?tag=84#documents-list)\n    #8. https://www.asep.gob.pa/wp-content/uploads/electricidad/resoluciones/anno_12528_elec.pdf\n    for thermal_production_unit in thermal_production_units:\n      unit_name_and_generation = thermal_production_unit.find_all('td')\n      unit_name = unit_name_and_generation[0].string\n      unit_generation = float(unit_name_and_generation[1].string)\n      if(unit_name in map_thermal_generation_unit_name_to_fuel_type):\n        if(unit_generation > 0):#Ignore self-consumption\n          unit_fuel_type = map_thermal_generation_unit_name_to_fuel_type[unit_name]\n          data['production'][unit_fuel_type] += unit_generation\n          data['production']['unknown'] -= unit_generation\n      else:\n        logger.warning(u'{} is not mapped to generation type'.format(unit_name), extra={'key': zone_key})\n\n    #Thermal total from the graph and the total one would get from summing output of all generators deviates a bit,\n    #presumably because they aren't updated at the exact same moment.\n    #Because negative production causes an error with ElectricityMap, we'll ignore small amounts of negative production\n    #TODO we might want to use the sum of the production of all thermal units instead of this workaround,\n    #because now we're still reporting small *postive* amounts of \"ghost\" thermal production\n    if data['production']['unknown'] < 0 and data['production']['unknown'] > -10:\n      logger.info(f\"Ignoring small amount of negative thermal generation ({data['production']['unknown']}MW)\", extra={\"key\": zone_key})\n      data['production']['unknown'] = 0\n\n    #Round remaining \"unknown\" output to 13 decimal places to get rid of floating point errors\n    data['production']['unknown'] = round(data['production']['unknown'],13)\n\n    # Parse the datetime and return a python datetime object\n    spanish_date = soup.find('h3', {'class': 'sitr-update'}).string\n    date = arrow.get(spanish_date, 'DD-MMMM-YYYY H:mm:ss', locale=\"es\", tzinfo=\"America/Panama\")\n    data['datetime'] = date.datetime\n\n    return data\n\n\nif __name__ == '__main__':\n    \"\"\"Main method, never used by the Electricity Map backend, but handy for testing.\"\"\"\n\n    print('fetch_production() ->')\n    print(fetch_production())\n", "path": "parsers/PA.py"}]}