cetk 0.0.4.dev0__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.
- cetk/__init__.py +87 -0
- cetk/db.py +41 -0
- cetk/edb/__init__.py +0 -0
- cetk/edb/apps.py +9 -0
- cetk/edb/cache.py +249 -0
- cetk/edb/const.py +26 -0
- cetk/edb/copy.py +18 -0
- cetk/edb/exporters.py +551 -0
- cetk/edb/importers/__init__.py +36 -0
- cetk/edb/importers/activity_import.py +279 -0
- cetk/edb/importers/codeset_import.py +124 -0
- cetk/edb/importers/gridsource_import.py +256 -0
- cetk/edb/importers/roadsource_import.py +1602 -0
- cetk/edb/importers/source_import.py +958 -0
- cetk/edb/importers/timevar_import.py +115 -0
- cetk/edb/importers/utils.py +92 -0
- cetk/edb/importers/validation.py +207 -0
- cetk/edb/ltreefield.py +76 -0
- cetk/edb/migrations/0001_initial.py +1731 -0
- cetk/edb/migrations/0002_data.py +156 -0
- cetk/edb/migrations/0003_create_gpkg_tables.py +14 -0
- cetk/edb/migrations/__init__.py +0 -0
- cetk/edb/models/__init__.py +49 -0
- cetk/edb/models/base.py +52 -0
- cetk/edb/models/common_models.py +144 -0
- cetk/edb/models/fleets.py +160 -0
- cetk/edb/models/gridsource_models.py +163 -0
- cetk/edb/models/road_classes.py +197 -0
- cetk/edb/models/road_models.py +413 -0
- cetk/edb/models/source_models.py +399 -0
- cetk/edb/models/timevar_models.py +111 -0
- cetk/edb/rasterize/__init__.py +1 -0
- cetk/edb/rasterize/rasterizer.py +1544 -0
- cetk/edb/signals.py +43 -0
- cetk/edb/traffic.py +75 -0
- cetk/edb/units.py +160 -0
- cetk/emissions/__init__.py +0 -0
- cetk/emissions/aggregate_emissions.sql +303 -0
- cetk/emissions/areasource_emissions.sql +53 -0
- cetk/emissions/calc.py +148 -0
- cetk/emissions/filters.py +179 -0
- cetk/emissions/gridsource_emissions.sql +52 -0
- cetk/emissions/pointsource_emissions.sql +53 -0
- cetk/emissions/queries.py +221 -0
- cetk/emissions/roadsource_emissions.sql +211 -0
- cetk/emissions/used_substances.sql +56 -0
- cetk/emissions/views.py +89 -0
- cetk/logging.py +68 -0
- cetk/settings.py +85 -0
- cetk/tools/__init__.py +1 -0
- cetk/tools/cetk_command.py +599 -0
- cetk/tools/manage.py +24 -0
- cetk/tools/utils.py +334 -0
- cetk/utils.py +62 -0
- cetk-0.0.4.dev0.dist-info/LICENSE +201 -0
- cetk-0.0.4.dev0.dist-info/METADATA +297 -0
- cetk-0.0.4.dev0.dist-info/RECORD +60 -0
- cetk-0.0.4.dev0.dist-info/WHEEL +5 -0
- cetk-0.0.4.dev0.dist-info/entry_points.txt +3 -0
- cetk-0.0.4.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from django.db import IntegrityError
|
|
3
|
+
|
|
4
|
+
from cetk.edb.models import ColdstartTimevar, FlowTimevar, Timevar
|
|
5
|
+
|
|
6
|
+
from .utils import import_error, worksheet_to_dataframe
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def import_timevarsheet(workbook, validation=False, sheetname="Timevar"):
|
|
10
|
+
return_message = []
|
|
11
|
+
timevar_data = workbook[sheetname].values
|
|
12
|
+
df_timevar = worksheet_to_dataframe(timevar_data)
|
|
13
|
+
if sheetname == "Timevar":
|
|
14
|
+
keyname = "emission"
|
|
15
|
+
if sheetname == "FlowTimevar":
|
|
16
|
+
keyname = "flow"
|
|
17
|
+
if sheetname == "ColdstartTimevar":
|
|
18
|
+
keyname = "coldstart"
|
|
19
|
+
timevar_dict = {keyname: {}}
|
|
20
|
+
|
|
21
|
+
# NB this only works if Excel file has exact same format
|
|
22
|
+
nr_timevars = (len(df_timevar["ID"]) + 1) // 27
|
|
23
|
+
for i in range(nr_timevars):
|
|
24
|
+
label = df_timevar["ID"][i * 27]
|
|
25
|
+
typeday = np.asarray(
|
|
26
|
+
df_timevar[
|
|
27
|
+
[
|
|
28
|
+
"monday",
|
|
29
|
+
"tuesday",
|
|
30
|
+
"wednesday",
|
|
31
|
+
"thursday",
|
|
32
|
+
"friday",
|
|
33
|
+
"saturday",
|
|
34
|
+
"sunday",
|
|
35
|
+
]
|
|
36
|
+
][i * 27 : i * 27 + 24]
|
|
37
|
+
)
|
|
38
|
+
month = np.asarray(df_timevar.iloc[i * 27 + 25, 2:14])
|
|
39
|
+
typeday_str = np.array2string(typeday).replace("\n", "").replace(" ", ", ")
|
|
40
|
+
month_str = np.array2string(month).replace("\n", "").replace(" ", ", ")
|
|
41
|
+
timevar_dict[keyname].update(
|
|
42
|
+
{label: {"typeday": typeday_str, "month": month_str}}
|
|
43
|
+
)
|
|
44
|
+
tv, return_append = import_timevars(
|
|
45
|
+
timevar_dict, overwrite=True, validation=validation
|
|
46
|
+
)
|
|
47
|
+
return_message += return_append
|
|
48
|
+
return_dict = {"timevar": {"updated or created": len(tv[keyname])}}
|
|
49
|
+
return return_dict, return_message
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def import_timevars(timevar_data, overwrite=False, validation=False):
|
|
53
|
+
"""import time-variation profiles."""
|
|
54
|
+
|
|
55
|
+
# Timevar instances must not be created by bulk_create as the save function
|
|
56
|
+
# is overloaded to calculate the normation constant.
|
|
57
|
+
def make_timevar(data, timevarclass, subname=None, validation=False):
|
|
58
|
+
retdict = {}
|
|
59
|
+
for name, timevar_data in data.items():
|
|
60
|
+
try:
|
|
61
|
+
typeday = timevar_data["typeday"]
|
|
62
|
+
month = timevar_data["month"]
|
|
63
|
+
if type(typeday) == list:
|
|
64
|
+
typeday = str(typeday)
|
|
65
|
+
if type(month) == list:
|
|
66
|
+
month = str(month)
|
|
67
|
+
if overwrite:
|
|
68
|
+
newobj, _ = timevarclass.objects.update_or_create(
|
|
69
|
+
name=name,
|
|
70
|
+
defaults={"typeday": typeday, "month": month},
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
newobj = timevarclass.objects.create(
|
|
75
|
+
name=name, typeday=typeday, month=month
|
|
76
|
+
)
|
|
77
|
+
except IntegrityError:
|
|
78
|
+
raise IntegrityError(
|
|
79
|
+
f"{timevarclass.__name__} {name} "
|
|
80
|
+
f"already exist in inventory."
|
|
81
|
+
)
|
|
82
|
+
retdict[name] = newobj
|
|
83
|
+
except KeyError:
|
|
84
|
+
return_message.append(
|
|
85
|
+
import_error(
|
|
86
|
+
f"Invalid specification of timevar {name}"
|
|
87
|
+
f", are 'typeday' and 'month' given?",
|
|
88
|
+
validation=validation,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
return retdict
|
|
92
|
+
|
|
93
|
+
return_message = []
|
|
94
|
+
timevars = {}
|
|
95
|
+
for vartype, subdict in timevar_data.items():
|
|
96
|
+
if vartype == "emission":
|
|
97
|
+
timevars.setdefault("emission", {}).update(
|
|
98
|
+
make_timevar(timevar_data[vartype], Timevar)
|
|
99
|
+
)
|
|
100
|
+
elif vartype == "flow":
|
|
101
|
+
timevars.setdefault("flow", {}).update(
|
|
102
|
+
make_timevar(timevar_data[vartype], FlowTimevar)
|
|
103
|
+
)
|
|
104
|
+
elif vartype == "coldstart":
|
|
105
|
+
timevars.setdefault("coldstart", {}).update(
|
|
106
|
+
make_timevar(timevar_data[vartype], ColdstartTimevar)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
return_message.append(
|
|
110
|
+
import_error(
|
|
111
|
+
f"invalid time-variation type '{vartype}' specified",
|
|
112
|
+
validation=validation,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
return timevars, return_message
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from cetk.edb.cache import cache_queryset
|
|
4
|
+
from cetk.edb.models import Settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ValidationError(Exception):
|
|
8
|
+
"""Error while validating emission data."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EmptySheet(Exception):
|
|
14
|
+
"""Error trying to read data from an empty sheet."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def nan2None(d):
|
|
20
|
+
out = d.copy()
|
|
21
|
+
for k, val in out.items():
|
|
22
|
+
out[k] = val if pd.notna(k) else val
|
|
23
|
+
return out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def import_error(message, return_message="", validation=False):
|
|
27
|
+
"""import error management"""
|
|
28
|
+
if not validation:
|
|
29
|
+
raise ValidationError(f"VALIDATION: {message}")
|
|
30
|
+
else:
|
|
31
|
+
return_message += f"VALIDATION: {message}\n"
|
|
32
|
+
return return_message
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def import_row_error(msg, row_nr, validation=False):
|
|
36
|
+
"""import error managment with row nr"""
|
|
37
|
+
return import_error(f"{msg}, on row {row_nr}", validation=validation)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def import_row_substance_error(msg, row_nr, substance, validation=False):
|
|
41
|
+
"""import error managment with row nr"""
|
|
42
|
+
return import_error(
|
|
43
|
+
f"{msg}, for '{substance}' on row {row_nr}", validation=validation
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cache_codeset(code_set):
|
|
48
|
+
"""return dict {code: activitycode} for codeset."""
|
|
49
|
+
if code_set is None:
|
|
50
|
+
return {}
|
|
51
|
+
return cache_queryset(code_set.codes.all(), "code")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cache_codesets():
|
|
55
|
+
"""
|
|
56
|
+
return list of dictionaries with activity-codes
|
|
57
|
+
for all code-sets in Settings.
|
|
58
|
+
"""
|
|
59
|
+
settings = Settings.get_current()
|
|
60
|
+
code_sets = {}
|
|
61
|
+
if settings.codeset1 is not None:
|
|
62
|
+
code_sets[settings.codeset1.slug] = cache_codeset(settings.codeset1)
|
|
63
|
+
if settings.codeset2 is not None:
|
|
64
|
+
code_sets[settings.codeset2.slug] = cache_codeset(settings.codeset2)
|
|
65
|
+
if settings.codeset3 is not None:
|
|
66
|
+
code_sets[settings.codeset3.slug] = cache_codeset(settings.codeset3)
|
|
67
|
+
return code_sets
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def worksheet_to_dataframe(data):
|
|
71
|
+
data = list(data)
|
|
72
|
+
if not data:
|
|
73
|
+
raise EmptySheet("Sheet is empty")
|
|
74
|
+
df = pd.DataFrame(data)
|
|
75
|
+
# Set the first row as the header
|
|
76
|
+
df.columns = df.iloc[0]
|
|
77
|
+
df = df[1:].reset_index(drop=True)
|
|
78
|
+
# Remove completely empty rows
|
|
79
|
+
df = df.dropna(how="all")
|
|
80
|
+
# Remove completely empty columns without a header
|
|
81
|
+
df = df.loc[:, ~(df.isna().all() & df.columns.isna())]
|
|
82
|
+
return df
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_substance_emission_columns(df):
|
|
86
|
+
"""return columns with substance emissions."""
|
|
87
|
+
return [col for col in df.columns if col.startswith("subst:")]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_activity_rate_columns(df):
|
|
91
|
+
"""return columns with activity rates."""
|
|
92
|
+
return [col for col in df.columns if col.startswith("act:")]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from os.path import basename, splitext
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import rasterio as rio
|
|
6
|
+
|
|
7
|
+
from cetk.edb.units import emission_unit_to_si
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def with_rownr(msg, row_nr):
|
|
11
|
+
return f"{msg}, at row {row_nr}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def with_rownr_and_substance(msg, row_nr, substance):
|
|
15
|
+
return f"{msg}, for '{substance}' at row {row_nr}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_columns(
|
|
19
|
+
df, required_cols, code_sets=None, substances=None, activities=None
|
|
20
|
+
):
|
|
21
|
+
messages = []
|
|
22
|
+
missing_cols = [
|
|
23
|
+
col for col in required_cols if col not in df.columns and df.index.name != col
|
|
24
|
+
]
|
|
25
|
+
if len(missing_cols) > 0:
|
|
26
|
+
messages.append(
|
|
27
|
+
f"GridSource: Missing required columns: '{', '.join(missing_cols)}'"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if code_sets is not None:
|
|
31
|
+
for codeset_slug in code_sets:
|
|
32
|
+
col_name = f"activitycode_{codeset_slug}"
|
|
33
|
+
if col_name not in df.columns:
|
|
34
|
+
messages.append(
|
|
35
|
+
f"GridSource: Missing column for code-set '{codeset_slug}'"
|
|
36
|
+
)
|
|
37
|
+
if substances is not None:
|
|
38
|
+
expected_substances = {
|
|
39
|
+
col[6:] for col in df.columns if col.startswith("subst:")
|
|
40
|
+
}
|
|
41
|
+
invalid_substances = expected_substances.difference(substances.keys())
|
|
42
|
+
if len(invalid_substances) > 0:
|
|
43
|
+
messages.append(f"GridSource: Invalid substances: {invalid_substances}")
|
|
44
|
+
|
|
45
|
+
if activities is not None:
|
|
46
|
+
expected_activities = {
|
|
47
|
+
col[4:].strip() for col in df.columns if col.startswith("act:")
|
|
48
|
+
}
|
|
49
|
+
invalid_activities = expected_activities.difference(activities.keys())
|
|
50
|
+
if len(invalid_activities) > 0:
|
|
51
|
+
messages.append(f"GridSource: Invalid activities: {invalid_activities}")
|
|
52
|
+
return messages
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_unit(values, row_nr):
|
|
56
|
+
"""validate substance emission unit."""
|
|
57
|
+
messages = []
|
|
58
|
+
unit = values["emission_unit"]
|
|
59
|
+
try:
|
|
60
|
+
emission_unit_to_si(1.0, unit)
|
|
61
|
+
except (KeyError, ValueError) as err:
|
|
62
|
+
messages.append(with_rownr(f"GridSource: invalid unit, {err}", row_nr))
|
|
63
|
+
return messages
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_activitycodes(values, code_sets, row_nr, src=None):
|
|
67
|
+
"""validate and set activity codes."""
|
|
68
|
+
messages = []
|
|
69
|
+
# get activitycodes
|
|
70
|
+
for code_ind, (codeset_slug, codes) in enumerate(code_sets.items(), 1):
|
|
71
|
+
code_attribute = f"activitycode{code_ind}"
|
|
72
|
+
code_col = f"activitycode_{codeset_slug}"
|
|
73
|
+
code = values[code_col]
|
|
74
|
+
if code is None:
|
|
75
|
+
messages.append(f"GridSource: missing value in column '{code_col}'")
|
|
76
|
+
try:
|
|
77
|
+
activitycode = codes[code]
|
|
78
|
+
except KeyError:
|
|
79
|
+
messages.append(f"GridSource: unknown code '{code}' in column '{code_col}'")
|
|
80
|
+
if src is not None:
|
|
81
|
+
setattr(src, code_attribute, activitycode)
|
|
82
|
+
return [with_rownr(msg, row_nr) for msg in messages]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_timevar(values, timevars, row_nr, src=None):
|
|
86
|
+
"""validate and set timevar."""
|
|
87
|
+
timevar_name = values["timevar"]
|
|
88
|
+
messages = []
|
|
89
|
+
if pd.notna(timevar_name):
|
|
90
|
+
try:
|
|
91
|
+
timevar = timevars[timevar_name]
|
|
92
|
+
except KeyError:
|
|
93
|
+
messages.append(f"GridSource: timevar '{timevar_name}' does not exist")
|
|
94
|
+
else:
|
|
95
|
+
timevar = None
|
|
96
|
+
if src is not None:
|
|
97
|
+
src.timevar = timevar
|
|
98
|
+
return [with_rownr(msg, row_nr) for msg in messages]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def data_to_raster(raster_name, raster_path, datadir, substance):
|
|
102
|
+
if raster_path is not None:
|
|
103
|
+
# generate raster name form path
|
|
104
|
+
if raster_name is None:
|
|
105
|
+
raster_name = splitext(basename(raster_path))[0]
|
|
106
|
+
# if placeholder is used for substance in path
|
|
107
|
+
# the raster-name will be given a suffix
|
|
108
|
+
|
|
109
|
+
if "{subst}" in raster_path:
|
|
110
|
+
if "{subst}" not in raster_name:
|
|
111
|
+
raster_name += "-{subst}"
|
|
112
|
+
rpath = raster_path.format(subst=substance)
|
|
113
|
+
rname = raster_name.format(subst=substance)
|
|
114
|
+
else:
|
|
115
|
+
rpath = raster_path
|
|
116
|
+
rname = raster_name
|
|
117
|
+
|
|
118
|
+
if not os.path.isabs(rpath):
|
|
119
|
+
rpath = os.path.join(datadir, rpath)
|
|
120
|
+
else:
|
|
121
|
+
rpath = None
|
|
122
|
+
rname = (
|
|
123
|
+
raster_name.format(subst=substance)
|
|
124
|
+
if "{subst}" in raster_name
|
|
125
|
+
else raster_name
|
|
126
|
+
)
|
|
127
|
+
return rname, rpath
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_raster(
|
|
131
|
+
values: dict,
|
|
132
|
+
raster_names: list,
|
|
133
|
+
datadir: str,
|
|
134
|
+
raster_dict: dict,
|
|
135
|
+
row_nr: int,
|
|
136
|
+
substance: str,
|
|
137
|
+
):
|
|
138
|
+
messages = []
|
|
139
|
+
rasters_without_path = []
|
|
140
|
+
raster_path = values["path"] if not pd.isna(values["path"]) else None
|
|
141
|
+
raster_name = values["rastername"] if not pd.isna(values["rastername"]) else None
|
|
142
|
+
if raster_name is None and raster_path is None:
|
|
143
|
+
messages.append("neither rastername nor path specified")
|
|
144
|
+
rname, rpath = data_to_raster(raster_name, raster_path, datadir, substance)
|
|
145
|
+
if rpath is not None:
|
|
146
|
+
if not os.path.exists(rpath):
|
|
147
|
+
messages.append(f"raster with path {rpath} does not exist")
|
|
148
|
+
elif rname not in raster_dict:
|
|
149
|
+
with rio.open(rpath, "r") as dset:
|
|
150
|
+
try:
|
|
151
|
+
_ = int(dset.crs.to_authority()[1])
|
|
152
|
+
except ValueError:
|
|
153
|
+
messages.append(
|
|
154
|
+
"raster coordinate reference system has no epsg code"
|
|
155
|
+
)
|
|
156
|
+
rdata = dset.read()
|
|
157
|
+
raster_dict.setdefault(rname, {})["sum"] = rdata[rdata != dset.nodata].sum()
|
|
158
|
+
raster_dict[rname]["path"] = rpath
|
|
159
|
+
else:
|
|
160
|
+
rasters_without_path.append(rname)
|
|
161
|
+
|
|
162
|
+
for rname in rasters_without_path:
|
|
163
|
+
if rname not in raster_names and rname not in raster_dict:
|
|
164
|
+
messages.append(
|
|
165
|
+
f"raster '{rname}' not found in database, and no path specified"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return [with_rownr_and_substance(msg, row_nr, substance) for msg in messages]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_emission(values, row_nr, substance):
|
|
172
|
+
"""validate emission value"""
|
|
173
|
+
messages = []
|
|
174
|
+
emis_value = values[f"subst:{substance}"]
|
|
175
|
+
if emis_value != "sum":
|
|
176
|
+
try:
|
|
177
|
+
val = float(emis_value)
|
|
178
|
+
if val < 0:
|
|
179
|
+
messages.append(
|
|
180
|
+
with_rownr_and_substance("emission value < 0", row_nr, substance)
|
|
181
|
+
)
|
|
182
|
+
except ValueError:
|
|
183
|
+
messages.append(
|
|
184
|
+
with_rownr_and_substance(
|
|
185
|
+
f"Invalid emission value '{emis_value}'", row_nr, substance
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
return messages
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def validate_activity(values, row_nr, activity):
|
|
192
|
+
"""validate emission value"""
|
|
193
|
+
messages = []
|
|
194
|
+
rate = values[f"act:{activity}"]
|
|
195
|
+
if rate != "sum":
|
|
196
|
+
try:
|
|
197
|
+
if float(rate) < 0:
|
|
198
|
+
messages.append(
|
|
199
|
+
with_rownr_and_substance("activity rate < 0", row_nr, activity)
|
|
200
|
+
)
|
|
201
|
+
except ValueError:
|
|
202
|
+
messages.append(
|
|
203
|
+
with_rownr_and_substance(
|
|
204
|
+
f"invalid activity rate '{rate}'", row_nr, activity
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
return messages
|
cetk/edb/ltreefield.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""A django field definition for Postgres hierarchical ltree field.
|
|
2
|
+
|
|
3
|
+
Source: https://github.com/whitglint/ltreefield
|
|
4
|
+
license: MIT, https://github.com/whitglint/ltreefield/blob/master/LICENSE
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.db.models import Lookup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LtreeField(models.CharField):
|
|
12
|
+
description = "SQLite alternative to ltree (up to %(max_length)s)"
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
kwargs["max_length"] = 256
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
# def db_type(self, connection):
|
|
19
|
+
# return "ltree"
|
|
20
|
+
|
|
21
|
+
def deconstruct(self):
|
|
22
|
+
name, path, args, kwargs = super().deconstruct()
|
|
23
|
+
del kwargs["max_length"]
|
|
24
|
+
return name, path, args, kwargs
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AncestorOrEqual(Lookup):
|
|
28
|
+
lookup_name = "aore"
|
|
29
|
+
|
|
30
|
+
def as_sql(self, qn, connection):
|
|
31
|
+
lhs, lhs_params = self.process_lhs(qn, connection)
|
|
32
|
+
rhs, rhs_params = self.process_rhs(qn, connection)
|
|
33
|
+
params = lhs_params + rhs_params
|
|
34
|
+
if len(rhs_params) > 1:
|
|
35
|
+
raise ValueError("This alternative for LTree is not implemented")
|
|
36
|
+
elif len(rhs_params[0]) > 2:
|
|
37
|
+
return (
|
|
38
|
+
'%s like %s or %s like %s or %s like "" '
|
|
39
|
+
% (lhs, rhs, lhs, rhs_params[0][:-2], lhs),
|
|
40
|
+
params,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
return '%s like %s or %s like "" ' % (lhs, rhs, lhs), params
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
LtreeField.register_lookup(AncestorOrEqual)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DescendantOrEqual(Lookup):
|
|
50
|
+
lookup_name = "dore"
|
|
51
|
+
|
|
52
|
+
def as_sql(self, qn, connection):
|
|
53
|
+
lhs, lhs_params = self.process_lhs(qn, connection)
|
|
54
|
+
rhs, rhs_params = self.process_rhs(qn, connection)
|
|
55
|
+
params = lhs_params + rhs_params
|
|
56
|
+
if len(params) > 1:
|
|
57
|
+
raise ValueError("This alternative for LTree is not implemented")
|
|
58
|
+
else:
|
|
59
|
+
params = [params[0] + "%"]
|
|
60
|
+
return "%s like %s" % (lhs, rhs), params
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
LtreeField.register_lookup(DescendantOrEqual)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Match(Lookup):
|
|
67
|
+
lookup_name = "match"
|
|
68
|
+
|
|
69
|
+
def as_sql(self, qn, connection):
|
|
70
|
+
lhs, lhs_params = self.process_lhs(qn, connection)
|
|
71
|
+
rhs, rhs_params = self.process_rhs(qn, connection)
|
|
72
|
+
params = lhs_params + rhs_params
|
|
73
|
+
return "%s like %s" % (lhs, rhs), params
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
LtreeField.register_lookup(Match)
|