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
cetk/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""cetk, a python library for editing Clair emission inventories offline."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.4.dev"
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import django
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
|
|
10
|
+
from cetk import logging
|
|
11
|
+
|
|
12
|
+
DEFAULT_SETTINGS = {
|
|
13
|
+
"DEBUG": False,
|
|
14
|
+
"INSTALLED_APPS": ["django.contrib.gis", "cetk.edb.apps.EdbConfig"],
|
|
15
|
+
"LANGUAGE_CODE": "en-us",
|
|
16
|
+
"TIME_ZONE": "UTC",
|
|
17
|
+
"USE_I18N": True,
|
|
18
|
+
"USE_TZ": True,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def configure():
|
|
25
|
+
if hasattr(settings, "configured") and not settings.configured:
|
|
26
|
+
default_config_home = os.path.expanduser("~/.config")
|
|
27
|
+
config_home = os.path.join(
|
|
28
|
+
os.environ.get("XDG_CONFIG_HOME", default_config_home), "eclair"
|
|
29
|
+
)
|
|
30
|
+
default_db = os.path.abspath(os.path.join(config_home, "eclair.gpkg"))
|
|
31
|
+
db_path = os.environ.get("CETK_DATABASE_PATH", default_db)
|
|
32
|
+
DEBUG = os.environ.get("CETK_DEBUG", False)
|
|
33
|
+
level = "DEBUG" if DEBUG else "INFO"
|
|
34
|
+
if level == "DEBUG":
|
|
35
|
+
cetk_logger = {"handlers": ["console_debug"], "level": level}
|
|
36
|
+
else:
|
|
37
|
+
cetk_logger = {"handlers": ["console"], "level": level}
|
|
38
|
+
|
|
39
|
+
if "FLATPAK_ID" in os.environ:
|
|
40
|
+
spatialite_path = os.environ.get(
|
|
41
|
+
"SPATIALITE_LIBRARY_PATH", "/app/lib/mod_spatialite.so"
|
|
42
|
+
)
|
|
43
|
+
elif os.name == "posix":
|
|
44
|
+
spatialite_path = os.environ.get(
|
|
45
|
+
"SPATIALITE_LIBRARY_PATH", "/usr/lib64/mod_spatialite.so"
|
|
46
|
+
)
|
|
47
|
+
elif os.name == "nt":
|
|
48
|
+
spatialite_path = os.environ.get(
|
|
49
|
+
"SPATIALITE_LIBRARY_PATH", r"C:\OSGeo4W\bin\mod_spatialite.dll"
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
spatialite_path = os.environ.get(
|
|
53
|
+
"SPATIALITE_LIBRARY_PATH", "/usr/lib64/mod_spatialite.so"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
settings.configure(
|
|
57
|
+
**DEFAULT_SETTINGS,
|
|
58
|
+
SPATIALITE_LIBRARY_PATH=spatialite_path,
|
|
59
|
+
DATABASES={
|
|
60
|
+
"default": {
|
|
61
|
+
"ENGINE": "django.contrib.gis.db.backends.spatialite",
|
|
62
|
+
"NAME": db_path,
|
|
63
|
+
"TEST": {"NAME": os.path.join(config_home, "test.eclair.gpkg")},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
LOGGING={
|
|
67
|
+
"version": 1,
|
|
68
|
+
"disable_existing_loggers": False,
|
|
69
|
+
"formatters": {
|
|
70
|
+
"cetk": {"format": "%(levelname)s: %(message)s"}, # noqa
|
|
71
|
+
"cetk_debug": {
|
|
72
|
+
"format": "%(asctime)s %(levelname)s: %(name)s %(message)s"
|
|
73
|
+
}, # noqa
|
|
74
|
+
},
|
|
75
|
+
"handlers": {
|
|
76
|
+
"console": {"class": "logging.StreamHandler", "formatter": "cetk"},
|
|
77
|
+
"console_debug": {
|
|
78
|
+
"class": "logging.StreamHandler",
|
|
79
|
+
"formatter": "cetk_debug",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"loggers": {"cetk": cetk_logger},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
django.setup()
|
|
86
|
+
log.debug("configured django")
|
|
87
|
+
return settings
|
cetk/db.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Database management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CetkDatabaseError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_migrate(db_path=None):
|
|
13
|
+
env = {**os.environ}
|
|
14
|
+
if db_path is not None:
|
|
15
|
+
env["CETK_DATABASE_PATH"] = str(db_path)
|
|
16
|
+
|
|
17
|
+
# handle error in geodjango starting with sqlite 3.36
|
|
18
|
+
# see: https://groups.google.com/g/spatialite-users/c/SnNZt4AGm_o
|
|
19
|
+
sqlite_version = list(map(int, sqlite3.sqlite_version.split(".")))
|
|
20
|
+
if sqlite_version[0] >= 3 and sqlite_version[1] >= 36:
|
|
21
|
+
proc = subprocess.run(
|
|
22
|
+
[
|
|
23
|
+
"cetkmanage",
|
|
24
|
+
"shell",
|
|
25
|
+
"-c",
|
|
26
|
+
"import django;django.db.connection.cursor().execute('SELECT InitSpatialMetaData(1);')", # noqa: E501
|
|
27
|
+
],
|
|
28
|
+
stdout=subprocess.PIPE,
|
|
29
|
+
stderr=subprocess.PIPE,
|
|
30
|
+
check=True,
|
|
31
|
+
env=env,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
proc = subprocess.run(
|
|
35
|
+
["cetkmanage", "migrate"],
|
|
36
|
+
stdout=subprocess.PIPE,
|
|
37
|
+
stderr=subprocess.PIPE,
|
|
38
|
+
check=True,
|
|
39
|
+
env=env,
|
|
40
|
+
)
|
|
41
|
+
return proc.stdout, proc.stderr
|
cetk/edb/__init__.py
ADDED
|
File without changes
|
cetk/edb/apps.py
ADDED
cetk/edb/cache.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Module to manage caching of emission data."""
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
|
|
8
|
+
# nr of records per page
|
|
9
|
+
PAGE_SIZE = 10000
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# source specific parameters to be added to dataset
|
|
13
|
+
# parameters must be included in emission query
|
|
14
|
+
STATIC_DATA_PARAMETERS = {
|
|
15
|
+
"road": [
|
|
16
|
+
"length",
|
|
17
|
+
"aadt",
|
|
18
|
+
"nolanes",
|
|
19
|
+
"speed",
|
|
20
|
+
"width",
|
|
21
|
+
"slope",
|
|
22
|
+
"heavy_vehicle_share",
|
|
23
|
+
],
|
|
24
|
+
"point": [
|
|
25
|
+
"chimney_height",
|
|
26
|
+
"chimney_outer_diameter",
|
|
27
|
+
"chimney_inner_diameter",
|
|
28
|
+
"chimney_gas_speed",
|
|
29
|
+
"chimney_gas_temperature",
|
|
30
|
+
"house_height",
|
|
31
|
+
"house_width",
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotInCacheError(Exception):
|
|
37
|
+
"""Page not found in cache"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EmissionCache:
|
|
41
|
+
|
|
42
|
+
"""An emission out-of-memory cache."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, querysets):
|
|
45
|
+
"""create an EmissionCache
|
|
46
|
+
|
|
47
|
+
args
|
|
48
|
+
querysets: dict with a sourcetype as key and a cursor queryset as value
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
self.cache_dir = None
|
|
53
|
+
|
|
54
|
+
# page count per sourcetype
|
|
55
|
+
self._pages = {}
|
|
56
|
+
|
|
57
|
+
# temporary in-memory storage
|
|
58
|
+
# write to "page" on disk when recs_in_memory > PAGE_SIZE
|
|
59
|
+
self._features = {}
|
|
60
|
+
self._weights = {}
|
|
61
|
+
self._emissions = {}
|
|
62
|
+
# counter for features
|
|
63
|
+
self._feature_index = {"point": 0, "road": 0}
|
|
64
|
+
self._rasterized_sources = set()
|
|
65
|
+
|
|
66
|
+
# nr of emission records not yet written to disk
|
|
67
|
+
self._recs_in_memory = {"road": 0, "point": 0, "area": 0, "grid": 0}
|
|
68
|
+
|
|
69
|
+
# map feature id to index in narc variable
|
|
70
|
+
self.feature_ids = {"road": {}, "point": {}}
|
|
71
|
+
|
|
72
|
+
# keep track of all sources that have already been gridded
|
|
73
|
+
self.gridded_sources = {
|
|
74
|
+
"road": set(),
|
|
75
|
+
"point": set(),
|
|
76
|
+
"area": set(),
|
|
77
|
+
"grid": set(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# create mapping between column name and index using a named tuple
|
|
81
|
+
self.col_maps = {}
|
|
82
|
+
for sourcetype, qs in querysets.items():
|
|
83
|
+
if qs is not None:
|
|
84
|
+
desc = qs.cursor.description
|
|
85
|
+
column_index_tuple = namedtuple("ColumnIndex", [col[0] for col in desc])
|
|
86
|
+
self.col_maps[sourcetype] = column_index_tuple(*range(len(desc)))
|
|
87
|
+
|
|
88
|
+
def __enter__(self):
|
|
89
|
+
# create temporary directory for cache
|
|
90
|
+
self.cache_dir = TemporaryDirectory(prefix="cache_")
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
94
|
+
if self.cache_dir is not None:
|
|
95
|
+
self.cache_dir.cleanup()
|
|
96
|
+
self.cache_dir = None
|
|
97
|
+
|
|
98
|
+
def has_sourcetype(self, sourcetype):
|
|
99
|
+
return sourcetype in self._pages
|
|
100
|
+
|
|
101
|
+
def has_substance(self, sourcetype, substance_id):
|
|
102
|
+
return self.emis_page_count(sourcetype, substance_id) > 0
|
|
103
|
+
|
|
104
|
+
def emis_page_count(self, sourcetype, substance_id):
|
|
105
|
+
return self._pages.get(sourcetype, {}).get(f"emission_{substance_id}", 0)
|
|
106
|
+
|
|
107
|
+
def weight_page_count(self, sourcetype):
|
|
108
|
+
return self._pages.get(sourcetype, {}).get("weights", 0)
|
|
109
|
+
|
|
110
|
+
def feature_page_count(self, sourcetype):
|
|
111
|
+
return self._pages.get(sourcetype, {}).get("features", 0)
|
|
112
|
+
|
|
113
|
+
def add_rec(
|
|
114
|
+
self, rec, sourcetype, *, write_feature=False, write_weights=False, weights=None
|
|
115
|
+
):
|
|
116
|
+
"""add feature to cache."""
|
|
117
|
+
|
|
118
|
+
col_map = self.col_maps[sourcetype]
|
|
119
|
+
# extract data from record
|
|
120
|
+
source_id = rec[col_map.source_id]
|
|
121
|
+
substance_id = rec[col_map.substance_id]
|
|
122
|
+
|
|
123
|
+
# only write to disk if source not already in self.feature_ids
|
|
124
|
+
# or in self.gridded_sources
|
|
125
|
+
# (to keep all data related to a specific source on the same cache page)
|
|
126
|
+
if (
|
|
127
|
+
self._recs_in_memory[sourcetype] > PAGE_SIZE
|
|
128
|
+
and source_id not in self.feature_ids[sourcetype]
|
|
129
|
+
and source_id not in self.gridded_sources[sourcetype]
|
|
130
|
+
):
|
|
131
|
+
if write_weights:
|
|
132
|
+
self.write_weights(sourcetype)
|
|
133
|
+
if write_feature:
|
|
134
|
+
self.write_features(sourcetype)
|
|
135
|
+
self.write_emissions(sourcetype)
|
|
136
|
+
|
|
137
|
+
# reset nr of records in memory
|
|
138
|
+
self._recs_in_memory[sourcetype] = 1
|
|
139
|
+
|
|
140
|
+
# emissions currently in memory
|
|
141
|
+
emissions = self._emissions.setdefault(sourcetype, {})
|
|
142
|
+
|
|
143
|
+
# add record
|
|
144
|
+
# avoid writing emission rec if source is gridded but there are no resulting
|
|
145
|
+
# weights. This may happen if emission filter polygon is bigger than dataset
|
|
146
|
+
# extent. The source is then included, but no weights are generated.
|
|
147
|
+
# It could potentially also happen due to uncertainties in coordinate
|
|
148
|
+
# transformation in PostGIS
|
|
149
|
+
if not (write_weights and weights is None):
|
|
150
|
+
emissions.setdefault(substance_id, []).append(rec)
|
|
151
|
+
|
|
152
|
+
self._recs_in_memory[sourcetype] += 1
|
|
153
|
+
|
|
154
|
+
if write_feature and source_id not in self.feature_ids[sourcetype]:
|
|
155
|
+
geom_wkt = rec[col_map.wkt]
|
|
156
|
+
params = STATIC_DATA_PARAMETERS[sourcetype]
|
|
157
|
+
data = {key: rec[getattr(col_map, key)] for key in params}
|
|
158
|
+
self.feature_ids[sourcetype][source_id] = self._feature_index[sourcetype]
|
|
159
|
+
# features currently in memory
|
|
160
|
+
features = self._features.setdefault(
|
|
161
|
+
sourcetype, {"ids": [], "geoms": [], "data": []}
|
|
162
|
+
)
|
|
163
|
+
features["ids"].append(source_id)
|
|
164
|
+
features["geoms"].append(geom_wkt)
|
|
165
|
+
features["data"].append(data)
|
|
166
|
+
self._feature_index[sourcetype] += 1
|
|
167
|
+
|
|
168
|
+
if sourcetype != "grid":
|
|
169
|
+
source_key = source_id
|
|
170
|
+
else:
|
|
171
|
+
raster = rec[col_map.raster]
|
|
172
|
+
source_key = (source_id, raster)
|
|
173
|
+
|
|
174
|
+
if weights is not None and source_key not in self.gridded_sources[sourcetype]:
|
|
175
|
+
self._weights.setdefault(sourcetype, {})[source_key] = weights
|
|
176
|
+
self.gridded_sources[sourcetype].add(source_key)
|
|
177
|
+
|
|
178
|
+
def write_features(self, sourcetype):
|
|
179
|
+
if sourcetype in self._features:
|
|
180
|
+
self._write_page_to_cache(
|
|
181
|
+
sourcetype, "features", self._features[sourcetype]
|
|
182
|
+
)
|
|
183
|
+
self._features[sourcetype] = {"ids": [], "geoms": [], "data": []}
|
|
184
|
+
|
|
185
|
+
def read_features(self, sourcetype, page_nr):
|
|
186
|
+
return self._read_page_from_cache(sourcetype, "features", page_nr)
|
|
187
|
+
|
|
188
|
+
def write_weights(self, sourcetype):
|
|
189
|
+
if sourcetype in self._weights:
|
|
190
|
+
self._write_page_to_cache(sourcetype, "weights", self._weights[sourcetype])
|
|
191
|
+
self._weights[sourcetype] = {}
|
|
192
|
+
|
|
193
|
+
def read_weights(self, sourcetype, page_nr):
|
|
194
|
+
return self._read_page_from_cache(sourcetype, "weights", page_nr)
|
|
195
|
+
|
|
196
|
+
def write_emissions(self, sourcetype):
|
|
197
|
+
if sourcetype in self._emissions:
|
|
198
|
+
for substance_id, data in self._emissions[sourcetype].items():
|
|
199
|
+
self._write_page_to_cache(sourcetype, f"emission_{substance_id}", data)
|
|
200
|
+
self._emissions[sourcetype] = {}
|
|
201
|
+
|
|
202
|
+
def read_emissions(self, sourcetype, substance_id, page_nr):
|
|
203
|
+
return self._read_page_from_cache(
|
|
204
|
+
sourcetype, f"emission_{substance_id}", page_nr
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _write_page_to_cache(self, sourcetype, namespace, data):
|
|
208
|
+
"""Write temporary data to disk"""
|
|
209
|
+
|
|
210
|
+
# if data is empty, do not write any page
|
|
211
|
+
if len(data) == 0:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# set page count for sourcetype in namespace
|
|
215
|
+
pages = self._pages.setdefault(sourcetype, {})
|
|
216
|
+
if namespace not in pages:
|
|
217
|
+
pages[namespace] = 1
|
|
218
|
+
else:
|
|
219
|
+
pages[namespace] += 1
|
|
220
|
+
|
|
221
|
+
page_nr = pages[namespace]
|
|
222
|
+
cache_subdir = Path(self.cache_dir.name) / sourcetype / namespace
|
|
223
|
+
cache_subdir.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
with cache_subdir.joinpath(f"page_{page_nr}.tmp").open("wb") as page_file:
|
|
225
|
+
pickle.dump(data, page_file, protocol=-1)
|
|
226
|
+
|
|
227
|
+
def _read_page_from_cache(self, sourcetype, namespace, page_nr):
|
|
228
|
+
"""Read page of data from disk."""
|
|
229
|
+
|
|
230
|
+
cache_subdir = Path(self.cache_dir.name) / sourcetype / namespace
|
|
231
|
+
page_file_name = cache_subdir / f"page_{page_nr}.tmp"
|
|
232
|
+
if not page_file_name.exists():
|
|
233
|
+
raise NotInCacheError(f"file {page_file_name} not found in cache")
|
|
234
|
+
with page_file_name.open("rb") as page_file:
|
|
235
|
+
return pickle.load(page_file)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def cache_queryset(queryset, fields):
|
|
239
|
+
"""Return dict of model instances with fields as key
|
|
240
|
+
If several fields are specified, a tuple of the fields is used as key
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def fields2key(inst, fields):
|
|
244
|
+
if hasattr(fields, "__iter__") and not isinstance(fields, str):
|
|
245
|
+
return tuple([getattr(inst, field) for field in fields])
|
|
246
|
+
else:
|
|
247
|
+
return getattr(inst, fields)
|
|
248
|
+
|
|
249
|
+
return dict(((fields2key(instance, fields), instance) for instance in queryset))
|
cetk/edb/const.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
WGS84_SRID = 4326
|
|
2
|
+
DEFAULT_SRID = 3857
|
|
3
|
+
CHAR_FIELD_LENGTH = 100
|
|
4
|
+
CODE_FIELD_LENGTH = 50
|
|
5
|
+
NODATA = -9999.0
|
|
6
|
+
DEFAULT_EMISSION_UNIT = "kg/year"
|
|
7
|
+
|
|
8
|
+
# sheet names which are valid for data import
|
|
9
|
+
SHEET_NAMES = [
|
|
10
|
+
"CodeSet",
|
|
11
|
+
"ActivityCode",
|
|
12
|
+
"EmissionFactor",
|
|
13
|
+
"Timevar",
|
|
14
|
+
"PointSource",
|
|
15
|
+
"AreaSource",
|
|
16
|
+
"GridSource",
|
|
17
|
+
"VehicleFuel",
|
|
18
|
+
"Fleet",
|
|
19
|
+
"CongestionProfile",
|
|
20
|
+
"FlowTimevar",
|
|
21
|
+
"ColdstartTimevar",
|
|
22
|
+
"RoadAttribute",
|
|
23
|
+
"TrafficSituation",
|
|
24
|
+
"VehicleEmissionFactor",
|
|
25
|
+
"RoadSource",
|
|
26
|
+
]
|
cetk/edb/copy.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# from django.db import transaction
|
|
2
|
+
|
|
3
|
+
# from cetk.edb import models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def copy_model_instance(instance, **updated_fields):
|
|
7
|
+
"""Create a copy of a model instance in the database."""
|
|
8
|
+
meta = instance._meta
|
|
9
|
+
copy = meta.model(
|
|
10
|
+
**{
|
|
11
|
+
f.name: updated_fields.get(f.name, getattr(instance, f.name))
|
|
12
|
+
for f in meta.get_fields()
|
|
13
|
+
if not (f.one_to_many or f.many_to_many)
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
copy.pk = None
|
|
17
|
+
copy.save()
|
|
18
|
+
return copy
|