chellow 1729081025.0.0__py3-none-any.whl → 1729755916.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of chellow might be problematic. Click here for more details.

@@ -0,0 +1,167 @@
1
+ from datetime import datetime as Datetime
2
+ from decimal import Decimal, InvalidOperation
3
+
4
+ from dateutil.relativedelta import relativedelta
5
+
6
+ from openpyxl import load_workbook
7
+
8
+ from werkzeug.exceptions import BadRequest
9
+
10
+ from chellow.utils import HH, parse_mpan_core, to_ct, to_utc
11
+
12
+
13
+ def get_cell(sheet, col, row):
14
+ try:
15
+ coordinates = f"{col}{row}"
16
+ return sheet[coordinates]
17
+ except IndexError:
18
+ raise BadRequest(f"Can't find the cell {coordinates} on sheet {sheet}.")
19
+
20
+
21
+ def get_date_ct(sheet, col, row):
22
+ date_str = get_str(sheet, col, row)
23
+ return to_ct(Datetime.strptime(date_str, "%d/%m/%Y"))
24
+
25
+
26
+ def get_str(sheet, col, row):
27
+ return get_cell(sheet, col, row).value.strip()
28
+
29
+
30
+ def get_dec(sheet, col, row):
31
+ cell = get_cell(sheet, col, row)
32
+ try:
33
+ return Decimal(str(cell.value))
34
+ except InvalidOperation as e:
35
+ raise BadRequest(f"Problem parsing the number at {cell.coordinate}. {e}")
36
+
37
+
38
+ def get_int(sheet, col, row):
39
+ return int(get_cell(sheet, col, row).value)
40
+
41
+
42
+ ELEM_LOOKUP = {
43
+ "Admin Fee": {
44
+ "gbp": "admin-gbp",
45
+ "units": None,
46
+ "rate": None,
47
+ },
48
+ "VAT": None,
49
+ "GDUOS Charges": {
50
+ "gbp": "duos-gbp",
51
+ "units": None,
52
+ "rate": None,
53
+ },
54
+ "Power Generation": {
55
+ "gbp": "ssp-gbp",
56
+ "units": "ssp-kwh",
57
+ "rate": "ssp-rate",
58
+ },
59
+ }
60
+
61
+
62
+ def _make_raw_bills(book):
63
+ bills = {}
64
+ mpan_lookup = {}
65
+ sheet = book.worksheets[0]
66
+ for row in range(2, len(sheet["A"]) + 1):
67
+ val = get_cell(sheet, "A", row).value
68
+ if val is None or val == "":
69
+ break
70
+
71
+ description = get_str(sheet, "T", row)
72
+ desc_parts = [d.strip() for d in description.split("-")]
73
+ if len(desc_parts) > 1:
74
+ start_date_ct = to_ct(Datetime.strptime(desc_parts[1], "%b %y"))
75
+ finish_date_ct = start_date_ct + relativedelta(months=1) - HH
76
+ else:
77
+ start_date_ct = get_date_ct(sheet, "M", row)
78
+ finish_date_ct = get_date_ct(sheet, "N", row) + relativedelta(
79
+ hours=23, minutes=30
80
+ )
81
+
82
+ desc_elem = desc_parts[0]
83
+ start_date = to_utc(start_date_ct)
84
+ finish_date = to_utc(finish_date_ct)
85
+ reference = get_str(sheet, "G", row)
86
+ issue_date = to_utc(get_date_ct(sheet, "I", row))
87
+
88
+ bill_key = reference, start_date
89
+ try:
90
+ bill = bills[bill_key]
91
+ except KeyError:
92
+ bill = bills[bill_key] = {
93
+ "bill_type_code": "N",
94
+ "kwh": Decimal(0),
95
+ "vat": Decimal("0.00"),
96
+ "net": Decimal("0.00"),
97
+ "gross": Decimal("0.00"),
98
+ "reads": [],
99
+ "breakdown": {},
100
+ "issue_date": issue_date,
101
+ "start_date": start_date,
102
+ "finish_date": finish_date,
103
+ "reference": reference,
104
+ }
105
+
106
+ net = round(get_dec(sheet, "V", row), 2)
107
+ vat = round(get_dec(sheet, "W", row), 2)
108
+ gross = round(get_dec(sheet, "X", row), 2)
109
+ bill["net"] += net
110
+ bill["vat"] += vat
111
+ bill["gross"] += gross
112
+
113
+ mpan_core_str = get_str(sheet, "E", row)
114
+ if len(mpan_core_str) > 0:
115
+ mpan_lookup[reference] = parse_mpan_core(mpan_core_str)
116
+
117
+ mpan_core = mpan_lookup[reference]
118
+
119
+ bill["mpan_core"] = mpan_core
120
+ bill["account"] = mpan_core
121
+
122
+ titles = ELEM_LOOKUP[desc_elem]
123
+ if titles is None:
124
+ continue
125
+
126
+ breakdown = bill["breakdown"]
127
+ breakdown[titles["gbp"]] = net
128
+
129
+ units_title = titles["units"]
130
+ rate_title = titles["rate"]
131
+ units = get_dec(sheet, "P", row)
132
+ rate = get_dec(sheet, "U", row) / Decimal(1000)
133
+
134
+ if units_title is not None:
135
+ breakdown[units_title] = units
136
+ if rate_title is not None:
137
+ breakdown[rate_title] = [rate]
138
+ return bills.values()
139
+
140
+
141
+ class Parser:
142
+ def __init__(self, f):
143
+ self.book = load_workbook(f, data_only=True)
144
+
145
+ self.last_line = None
146
+ self._line_number = None
147
+ self._title_line = None
148
+
149
+ @property
150
+ def line_number(self):
151
+ return None if self._line_number is None else self._line_number + 1
152
+
153
+ def _set_last_line(self, i, line):
154
+ self._line_number = i
155
+ self.last_line = line
156
+ if i == 0:
157
+ self._title_line = line
158
+ return line
159
+
160
+ def make_raw_bills(self):
161
+ row = bills = None
162
+ try:
163
+ bills = _make_raw_bills(self.book)
164
+ except BadRequest as e:
165
+ raise BadRequest(f"Row number: {row} {e.description}")
166
+
167
+ return bills
chellow/e/hh_importer.py CHANGED
@@ -40,7 +40,15 @@ from chellow.utils import (
40
40
  processes = defaultdict(list)
41
41
  tasks = {}
42
42
 
43
- extensions = [".df2", ".simple.csv", ".bg.csv", ".vital.xlsx", ".edf.csv"]
43
+ extensions = [
44
+ ".df2",
45
+ ".simple.csv",
46
+ ".bg.csv",
47
+ ".vital.xlsx",
48
+ ".edf.csv",
49
+ ".schneider.csv",
50
+ ".schneider.xlsx",
51
+ ]
44
52
 
45
53
 
46
54
  class HhDataImportProcess(threading.Thread):
@@ -0,0 +1,75 @@
1
+ import csv
2
+ import itertools
3
+ from codecs import iterdecode
4
+ from datetime import datetime as Datetime
5
+ from decimal import Decimal, InvalidOperation
6
+
7
+ from werkzeug.exceptions import BadRequest
8
+
9
+ from chellow.utils import parse_mpan_core, to_utc, validate_hh_start
10
+
11
+ # "Time stamp","Value","Events","Comment","User"
12
+ # "18/10/2024 09:30:00","88.0","","",""
13
+ # "18/10/2024 09:00:00","89.1","","",""
14
+
15
+
16
+ def create_parser(reader, mpan_map, messages):
17
+ return HhParserSchneiderCsv(reader, mpan_map, messages)
18
+
19
+
20
+ def get_field(values, index, name):
21
+ if len(values) > index:
22
+ return values[index].strip()
23
+ else:
24
+ raise BadRequest(f"Can't find field {index}, {name}.")
25
+
26
+
27
+ def parse_values(values):
28
+ start_date_str = get_field(values, 0, "Timestamp")
29
+ start_date = validate_hh_start(
30
+ to_utc(Datetime.strptime(start_date_str, "%d/%m/%Y %H:%M:%S"))
31
+ )
32
+
33
+ reading_str = get_field(values, 1, "Reading")
34
+ reading_str = reading_str.replace(",", "")
35
+ try:
36
+ reading = Decimal(reading_str)
37
+ except InvalidOperation as e:
38
+ raise BadRequest(f"Problem parsing the number {reading_str}. {e}")
39
+ return start_date, reading
40
+
41
+
42
+ class HhParserSchneiderCsv:
43
+ def __init__(self, reader, mpan_map, messages):
44
+ s = iterdecode(reader, "utf-8")
45
+ self.shredder = zip(itertools.count(1), csv.reader(s))
46
+ next(self.shredder) # skip the title line
47
+ self.line_number, self.values = next(self.shredder)
48
+ _, self.pres_reading = parse_values(self.values)
49
+ self.mpan_core = parse_mpan_core(next(iter(mpan_map.values())))
50
+
51
+ def __iter__(self):
52
+ return self
53
+
54
+ def __next__(self):
55
+ try:
56
+ self.line_number, self.values = next(self.shredder)
57
+ start_date, reading = parse_values(self.values)
58
+ datum = {
59
+ "mpan_core": self.mpan_core,
60
+ "channel_type": "ACTIVE",
61
+ "start_date": start_date,
62
+ "value": self.pres_reading - reading,
63
+ "status": "A",
64
+ }
65
+ self.pres_reading = reading
66
+ return datum
67
+ except BadRequest as e:
68
+ e.description = (
69
+ f"Problem at line number: {self.line_number}: {self.values}: "
70
+ f"{e.description}"
71
+ )
72
+ raise e
73
+
74
+ def close(self):
75
+ self.shredder.close()
@@ -0,0 +1,118 @@
1
+ from datetime import datetime as Datetime
2
+ from decimal import Decimal, InvalidOperation
3
+
4
+ from openpyxl import load_workbook
5
+
6
+ from werkzeug.exceptions import BadRequest
7
+
8
+ from chellow.utils import parse_mpan_core, to_ct, to_utc, validate_hh_start
9
+
10
+ # "Time stamp","Value","Events","Comment","User"
11
+ # "18/10/2024 09:30:00","88.0","","",""
12
+ # "18/10/2024 09:00:00","89.1","","",""
13
+
14
+
15
+ def create_parser(reader, mpan_map, messages):
16
+ return HhParserSchneiderXlsx(reader, mpan_map, messages)
17
+
18
+
19
+ def get_cell(sheet, col, row):
20
+ try:
21
+ coordinates = f"{col}{row}"
22
+ return sheet[coordinates]
23
+ except IndexError:
24
+ raise BadRequest(f"Can't find the cell {coordinates} on sheet {sheet}.")
25
+
26
+
27
+ def get_date(sheet, col, row):
28
+ cell = get_cell(sheet, col, row)
29
+ val = cell.value
30
+ if not isinstance(val, Datetime):
31
+ raise BadRequest(
32
+ f"Problem reading {val} (of type {type(val)}) as a timestamp at "
33
+ f"{cell.coordinate}."
34
+ )
35
+ return val
36
+
37
+
38
+ def get_str(sheet, col, row):
39
+ return get_cell(sheet, col, row).value.strip()
40
+
41
+
42
+ def get_dec(sheet, col, row):
43
+ return get_dec_from_cell(get_cell(sheet, col, row))
44
+
45
+
46
+ def get_dec_from_cell(cell):
47
+ try:
48
+ return Decimal(str(cell.value))
49
+ except InvalidOperation as e:
50
+ raise BadRequest(f"Problem parsing the number at {cell.coordinate}. {e}")
51
+
52
+
53
+ def get_int(sheet, col, row):
54
+ return int(get_cell(sheet, col, row).value)
55
+
56
+
57
+ def find_hhs(sheet, set_line_number, mpan_core):
58
+ pres_reading = None
59
+ for row in range(2, len(sheet["A"]) + 1):
60
+ set_line_number(row)
61
+ try:
62
+ timestamp_cell = get_cell(sheet, "A", row)
63
+ timestamp_str = timestamp_cell.value
64
+ if timestamp_str is None:
65
+ continue
66
+
67
+ # 2024-10-02 00:00:00 +1H, DST
68
+ ts_str = timestamp_str.split(",")[0]
69
+ ts_naive = Datetime.strptime(ts_str[:19], "%Y-%m-%d %H:%M:%S")
70
+ # Sometimes has seconds
71
+ ts_naive = Datetime(
72
+ ts_naive.year,
73
+ ts_naive.month,
74
+ ts_naive.day,
75
+ ts_naive.hour,
76
+ ts_naive.minute,
77
+ )
78
+ is_dst = ts_str[21] == 1
79
+ if is_dst:
80
+ start_date = to_utc(to_ct(ts_naive))
81
+ else:
82
+ start_date = to_utc(ts_naive)
83
+ start_date = validate_hh_start(start_date)
84
+
85
+ reading = get_dec(sheet, "B", row)
86
+
87
+ if pres_reading is not None:
88
+ value = pres_reading - reading
89
+ if value >= 0:
90
+ yield {
91
+ "mpan_core": mpan_core,
92
+ "channel_type": "ACTIVE",
93
+ "start_date": start_date,
94
+ "value": value,
95
+ "status": "A",
96
+ }
97
+ pres_reading = reading
98
+ except BadRequest as e:
99
+ e.description = f"Problem at line number: {row}: {e.description}"
100
+ raise e
101
+
102
+
103
+ class HhParserSchneiderXlsx:
104
+ def __init__(self, input_stream, mpan_map, messages):
105
+ book = load_workbook(input_stream, data_only=True)
106
+ self.sheet = book.worksheets[0]
107
+ imp_name = get_str(self.sheet, "B", 1).strip()
108
+ self.mpan_core = parse_mpan_core(mpan_map[imp_name])
109
+ self.line_number = 0
110
+
111
+ def _set_line_number(self, line_number):
112
+ self.line_number = line_number
113
+
114
+ def __iter__(self):
115
+ return find_hhs(self.sheet, self._set_line_number, self.mpan_core)
116
+
117
+ def close(self):
118
+ self.shredder.close()
chellow/e/views.py CHANGED
@@ -68,7 +68,6 @@ from chellow.models import (
68
68
  ReadType,
69
69
  RegisterRead,
70
70
  Report,
71
- Scenario,
72
71
  Site,
73
72
  SiteEra,
74
73
  Snag,
@@ -80,7 +79,6 @@ from chellow.models import (
80
79
  )
81
80
  from chellow.utils import (
82
81
  HH,
83
- c_months_c,
84
82
  c_months_u,
85
83
  csv_make_val,
86
84
  ct_datetime,
@@ -1220,7 +1218,7 @@ def dc_contract_edit_post(contract_id):
1220
1218
 
1221
1219
 
1222
1220
  @e.route("/dc_contracts/<int:contract_id>/hh_imports")
1223
- def dc_contracts_hh_imports_get(contract_id):
1221
+ def dc_contract_hh_imports_get(contract_id):
1224
1222
  contract = Contract.get_dc_by_id(g.sess, contract_id)
1225
1223
  processes = chellow.e.hh_importer.get_hh_import_processes(contract.id)
1226
1224
  return render_template(
@@ -1232,7 +1230,7 @@ def dc_contracts_hh_imports_get(contract_id):
1232
1230
 
1233
1231
 
1234
1232
  @e.route("/dc_contracts/<int:contract_id>/hh_imports", methods=["POST"])
1235
- def dc_contracts_hh_imports_post(contract_id):
1233
+ def dc_contract_hh_imports_post(contract_id):
1236
1234
  try:
1237
1235
  contract = Contract.get_dc_by_id(g.sess, contract_id)
1238
1236
 
@@ -3910,142 +3908,6 @@ def read_type_get(read_type_id):
3910
3908
  return render_template("read_type.html", read_type=read_type)
3911
3909
 
3912
3910
 
3913
- @e.route("/scenarios")
3914
- def scenarios_get():
3915
- scenarios = g.sess.query(Scenario).order_by(Scenario.name).all()
3916
- return render_template("scenarios.html", scenarios=scenarios)
3917
-
3918
-
3919
- @e.route("/scenarios/add", methods=["POST"])
3920
- def scenario_add_post():
3921
- try:
3922
- name = req_str("name")
3923
- properties = req_zish("properties")
3924
- scenario = Scenario.insert(g.sess, name, properties)
3925
- g.sess.commit()
3926
- return chellow_redirect(f"/scenarios/{scenario.id}", 303)
3927
- except BadRequest as e:
3928
- g.sess.rollback()
3929
- flash(e.description)
3930
- scenarios = g.sess.query(Scenario).order_by(Scenario.name)
3931
- return make_response(
3932
- render_template("scenario_add.html", scenarios=scenarios), 400
3933
- )
3934
-
3935
-
3936
- @e.route("/scenarios/add")
3937
- def scenario_add_get():
3938
- now = utc_datetime_now()
3939
- props = {
3940
- "scenario_start_month": now.month,
3941
- "scenario_start_year": now.year,
3942
- "scenario_duration": 1,
3943
- }
3944
- return render_template("scenario_add.html", initial_props=dumps(props))
3945
-
3946
-
3947
- @e.route("/scenarios/<int:scenario_id>")
3948
- def scenario_get(scenario_id):
3949
- start_date = None
3950
- finish_date = None
3951
- duration = 1
3952
-
3953
- scenario = Scenario.get_by_id(g.sess, scenario_id)
3954
- props = scenario.props
3955
- site_codes = "\n".join(props.get("site_codes", []))
3956
- try:
3957
- duration = props["scenario_duration"]
3958
- _, finish_date_ct = list(
3959
- c_months_c(
3960
- start_year=props["scenario_start_year"],
3961
- start_month=props["scenario_start_month"],
3962
- months=duration,
3963
- )
3964
- )[-1]
3965
- finish_date = to_utc(finish_date_ct)
3966
- except KeyError:
3967
- pass
3968
-
3969
- try:
3970
- start_date = to_utc(
3971
- ct_datetime(
3972
- props["scenario_start_year"],
3973
- props["scenario_start_month"],
3974
- props["scenario_start_day"],
3975
- props["scenario_start_hour"],
3976
- props["scenario_start_minute"],
3977
- )
3978
- )
3979
- except KeyError:
3980
- pass
3981
-
3982
- try:
3983
- finish_date = to_utc(
3984
- ct_datetime(
3985
- props["scenario_finish_year"],
3986
- props["scenario_finish_month"],
3987
- props["scenario_finish_day"],
3988
- props["scenario_finish_hour"],
3989
- props["scenario_finish_minute"],
3990
- )
3991
- )
3992
- except KeyError:
3993
- pass
3994
- return render_template(
3995
- "scenario.html",
3996
- scenario=scenario,
3997
- scenario_start_date=start_date,
3998
- scenario_finish_date=finish_date,
3999
- scenario_duration=duration,
4000
- site_codes=site_codes,
4001
- )
4002
-
4003
-
4004
- @e.route("/scenarios/<int:scenario_id>/edit")
4005
- def scenario_edit_get(scenario_id):
4006
- scenario = Scenario.get_by_id(g.sess, scenario_id)
4007
- return render_template("scenario_edit.html", scenario=scenario)
4008
-
4009
-
4010
- @e.route("/scenarios/<int:scenario_id>/edit", methods=["POST"])
4011
- def scenario_edit_post(scenario_id):
4012
- try:
4013
- scenario = Scenario.get_by_id(g.sess, scenario_id)
4014
- name = req_str("name")
4015
- properties = req_zish("properties")
4016
- scenario.update(name, properties)
4017
- g.sess.commit()
4018
- return chellow_redirect(f"/scenarios/{scenario.id}", 303)
4019
- except BadRequest as e:
4020
- g.sess.rollback()
4021
- description = e.description
4022
- flash(description)
4023
- return make_response(
4024
- render_template(
4025
- "scenario_edit.html",
4026
- scenario=scenario,
4027
- ),
4028
- 400,
4029
- )
4030
-
4031
-
4032
- @e.route("/scenarios/<int:scenario_id>/edit", methods=["DELETE"])
4033
- def scenario_edit_delete(scenario_id):
4034
- try:
4035
- scenario = Scenario.get_by_id(g.sess, scenario_id)
4036
- scenario.delete(g.sess)
4037
- g.sess.commit()
4038
- res = make_response()
4039
- res.headers["HX-Redirect"] = f"{chellow.utils.url_root}/e/scenarios"
4040
- return res
4041
- except BadRequest as e:
4042
- g.sess.rollback()
4043
- flash(e.description)
4044
- return make_response(
4045
- render_template("scenario_edit.html", scenario=scenario), 400
4046
- )
4047
-
4048
-
4049
3911
  @e.route("/sites/<int:site_id>/energy_management")
4050
3912
  def site_energy_management_get(site_id):
4051
3913
  site = Site.get_by_id(g.sess, site_id)