OpenGeodeWeb-Back 4.3.1__py3-none-any.whl → 5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: OpenGeodeWeb-Back
3
- Version: 4.3.1
3
+ Version: 5.0.0
4
4
  Summary: OpenGeodeWeb-Back is an open source framework that proposes handy python functions and wrappers for the OpenGeode ecosystem
5
5
  Author-email: Geode-solutions <team-web@geode-solutions.com>
6
6
  Project-URL: Homepage, https://github.com/Geode-solutions/OpenGeodeWeb-Back
@@ -12,31 +12,32 @@ Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: asgiref ==3.8.1
15
- Requires-Dist: attrs ==23.2.0
15
+ Requires-Dist: attrs ==24.2.0
16
16
  Requires-Dist: blinker ==1.8.2
17
17
  Requires-Dist: click ==8.1.7
18
18
  Requires-Dist: flask[async] ==3.0.3
19
19
  Requires-Dist: flask-cors ==4.0.1
20
- Requires-Dist: geode-background ==7.12.0
21
- Requires-Dist: geode-common ==31.2.1
22
- Requires-Dist: geode-conversion ==5.4.1
23
- Requires-Dist: geode-explicit ==4.8.3
24
- Requires-Dist: geode-implicit ==2.8.7
25
- Requires-Dist: geode-numerics ==4.4.0
26
- Requires-Dist: geode-simplex ==6.8.3
27
- Requires-Dist: geode-viewables ==2.2.3
20
+ Requires-Dist: geode-background ==8.1.1
21
+ Requires-Dist: geode-common ==32.0.8
22
+ Requires-Dist: geode-conversion ==6.0.3
23
+ Requires-Dist: geode-explicit ==6.0.1
24
+ Requires-Dist: geode-implicit ==3.1.1
25
+ Requires-Dist: geode-numerics ==5.0.2
26
+ Requires-Dist: geode-simplex ==8.2.1
27
+ Requires-Dist: geode-viewables ==3.0.0
28
28
  Requires-Dist: itsdangerous ==2.2.0
29
29
  Requires-Dist: jinja2 ==3.1.4
30
- Requires-Dist: jsonschema ==4.22.0
30
+ Requires-Dist: jsonschema ==4.23.0
31
31
  Requires-Dist: jsonschema-specifications ==2023.12.1
32
32
  Requires-Dist: markupsafe ==2.1.5
33
- Requires-Dist: opengeode-core ==14.21.3
34
- Requires-Dist: opengeode-geosciences ==7.7.0
35
- Requires-Dist: opengeode-geosciencesio ==4.7.7
36
- Requires-Dist: opengeode-inspector ==5.1.4
37
- Requires-Dist: opengeode-io ==6.5.1
33
+ Requires-Dist: opengeode-core ==15.2.1
34
+ Requires-Dist: opengeode-geosciences ==8.0.0
35
+ Requires-Dist: opengeode-geosciencesio ==5.0.1
36
+ Requires-Dist: opengeode-inspector ==6.0.0
37
+ Requires-Dist: opengeode-io ==7.0.1
38
38
  Requires-Dist: referencing ==0.35.1
39
- Requires-Dist: rpds-py ==0.18.1
39
+ Requires-Dist: rpds-py ==0.20.0
40
+ Requires-Dist: typing-extensions ==4.12.2
40
41
  Requires-Dist: werkzeug ==3.0.3
41
42
 
42
43
  <h1 align="center">OpenGeodeWeb-Back<sup><i>by Geode-solutions</i></sup></h1>
@@ -1,17 +1,20 @@
1
1
  opengeodeweb_back/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- opengeodeweb_back/geode_functions.py,sha256=aYYfnwiOc-NfixucoYIZUwNmrHe3xcGy4Ea_czLkG0Q,11637
2
+ opengeodeweb_back/app_config.py,sha256=bgn31kmMUhn6PwqpENPPQfsRsuXqnYgqMaV0_OoenDw,721
3
+ opengeodeweb_back/geode_functions.py,sha256=m1icB_E9SctF6LFoUDLxE0WseDy6wc1qNV6KeuBeEGo,8716
3
4
  opengeodeweb_back/geode_objects.py,sha256=8UMBmghw6aoJ4YdgzaRYdMInhPz_e7zfisP7xFIQyJo,20290
4
- opengeodeweb_back/routes/blueprint_routes.py,sha256=UnQZjGh-put0zqce--H8AgihmPzrUDNoaq2PSa-6EiA,7887
5
+ opengeodeweb_back/utils_functions.py,sha256=GTZllcyQT-DBfYXecxurRYa-CpBcOgpMvXqQvqNItZg,3920
6
+ opengeodeweb_back/routes/blueprint_routes.py,sha256=X_VF8pfxxci8Ek37uLjIDJTh9ItNiufd52hoBfbi9q8,8596
5
7
  opengeodeweb_back/routes/schemas/allowed_files.json,sha256=pRsGf39LiJpl3zEGLg4IqvRtm7iUx3Wq4Tb4tSFXGV0,234
6
8
  opengeodeweb_back/routes/schemas/allowed_objects.json,sha256=8JLtAI46eXeiJuiryS2geRVv0J1rGkFb87pRwtBZSZw,296
7
9
  opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.json,sha256=0t4YhdKxDlzcLh85JU85z6Pn5h8wlXVt3Zi4ZhXXmTQ,308
8
10
  opengeodeweb_back/routes/schemas/geographic_coordinate_systems.json,sha256=86QEBxAJXdMHulj2SyrxvAAwvyUq3mpKSazwASukeoM,242
9
11
  opengeodeweb_back/routes/schemas/inspect_file.json,sha256=7jmmLD2oZ2dxn5-2HqS6fU92eGM3FWBQdj3CjyYmGOA,285
10
12
  opengeodeweb_back/routes/schemas/missing_files.json,sha256=tJVdSM3CqYFZRC6eNW6Q3JG3RtoaZDxaZtbfx6djbX0,286
13
+ opengeodeweb_back/routes/schemas/ping.json,sha256=MhI5jtrjMsAsfIKEzdY8p1HyV9xv4O3hYfESWw6tkyE,162
11
14
  opengeodeweb_back/routes/schemas/save_viewable_file.json,sha256=7BXO8vsQrmqyEQ2uycm2Ift_EY7a0KocvnGEjYrQFcQ,368
12
15
  opengeodeweb_back/routes/schemas/upload_file.json,sha256=sE6bxz3mJbSZlGmrnR_hZmcx0dvZGn3Wpnn6szRPxXQ,186
13
- OpenGeodeWeb_Back-4.3.1.dist-info/LICENSE,sha256=LoTB-aqQvzTGxoTRXNnhNV0LKiqdk2bQv6MB34l8zkI,1079
14
- OpenGeodeWeb_Back-4.3.1.dist-info/METADATA,sha256=NlUTKTNXiFWQv3ilHs6Jb73Mk5cFWO7CCP7nkOkXJYc,3040
15
- OpenGeodeWeb_Back-4.3.1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
16
- OpenGeodeWeb_Back-4.3.1.dist-info/top_level.txt,sha256=tN1FZeLIVBrdja2-pbmhg5-tK-JILmmT9OeIBnhlUrQ,18
17
- OpenGeodeWeb_Back-4.3.1.dist-info/RECORD,,
16
+ OpenGeodeWeb_Back-5.0.0.dist-info/LICENSE,sha256=LoTB-aqQvzTGxoTRXNnhNV0LKiqdk2bQv6MB34l8zkI,1079
17
+ OpenGeodeWeb_Back-5.0.0.dist-info/METADATA,sha256=5C7g78DlkGcAQr6vddq97-RxyTehOlkXczT6HLd2Y30,3080
18
+ OpenGeodeWeb_Back-5.0.0.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
19
+ OpenGeodeWeb_Back-5.0.0.dist-info/top_level.txt,sha256=tN1FZeLIVBrdja2-pbmhg5-tK-JILmmT9OeIBnhlUrQ,18
20
+ OpenGeodeWeb_Back-5.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (74.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,33 @@
1
+ # Standard library imports
2
+ import os
3
+ import time
4
+
5
+ # Third party imports
6
+ # Local application imports
7
+
8
+
9
+ class Config(object):
10
+ FLASK_DEBUG = os.environ.get("FLASK_DEBUG", default=False)
11
+ DEFAULT_PORT = "5000"
12
+ CORS_HEADERS = "Content-Type"
13
+ UPLOAD_FOLDER = "./uploads"
14
+ DESKTOP_APP = False
15
+ REQUEST_COUNTER = 0
16
+ LAST_REQUEST_TIME = time.time()
17
+ LAST_PING_TIME = time.time()
18
+
19
+
20
+ class ProdConfig(Config):
21
+ SSL = None
22
+ ORIGINS = ""
23
+ MINUTES_BEFORE_TIMEOUT = "1"
24
+ SECONDS_BETWEEN_SHUTDOWNS = "10"
25
+ DATA_FOLDER_PATH = "/data/"
26
+
27
+
28
+ class DevConfig(Config):
29
+ SSL = None
30
+ ORIGINS = "*"
31
+ MINUTES_BEFORE_TIMEOUT = "1"
32
+ SECONDS_BETWEEN_SHUTDOWNS = "10"
33
+ DATA_FOLDER_PATH = "./data/"
@@ -1,20 +1,14 @@
1
1
  # Standard library imports
2
2
  import os
3
- import time
4
- import threading
5
- import uuid
6
- import zipfile
7
3
 
8
4
  # Third party imports
9
5
  import flask
10
6
  import opengeode_geosciences as og_gs
11
7
  import opengeode as og
12
- import pkg_resources
13
- from jsonschema import validate
14
- from jsonschema.exceptions import ValidationError
15
8
 
16
9
  # Local application imports
17
10
  from .geode_objects import geode_objects_dict
11
+ from . import utils_functions
18
12
 
19
13
 
20
14
  def geode_object_value(geode_object: str):
@@ -147,7 +141,9 @@ def list_geode_objects(
147
141
  key: str = None,
148
142
  ):
149
143
  return_dict = {}
150
- file_extension = extension_from_filename(os.path.basename(file_absolute_path))
144
+ file_extension = utils_functions.extension_from_filename(
145
+ os.path.basename(file_absolute_path)
146
+ )
151
147
  geode_objects_filtered_list = filter_geode_objects(key)
152
148
 
153
149
  for geode_object in geode_objects_filtered_list:
@@ -200,75 +196,6 @@ def get_inspector_children(obj):
200
196
  return new_object
201
197
 
202
198
 
203
- def versions(list_packages: list):
204
- list_with_versions = []
205
- for package in list_packages:
206
- list_with_versions.append(
207
- {
208
- "package": package,
209
- "version": pkg_resources.get_distribution(package).version,
210
- }
211
- )
212
- return list_with_versions
213
-
214
-
215
- def create_lock_file(
216
- folder_absolute_path,
217
- ):
218
- if not os.path.exists(folder_absolute_path):
219
- os.mkdir(folder_absolute_path)
220
- id = uuid.uuid4()
221
- file_absolute_path = f"{folder_absolute_path}/{str(id)}.txt"
222
- f = open(file_absolute_path, "a")
223
- f.close()
224
- flask.g.UUID = id
225
-
226
-
227
- def create_time_file(folder_absolute_path):
228
- if not os.path.exists(folder_absolute_path):
229
- os.mkdir(folder_absolute_path)
230
- file_path = f"{folder_absolute_path}/time.txt"
231
- if not os.path.isfile(file_path):
232
- f = open(file_path, "w")
233
- f.close()
234
-
235
- f = open(folder_absolute_path + "/time.txt", "w")
236
- f.write(str(time.time()))
237
- f.close()
238
-
239
-
240
- def remove_lock_file(folder_absolute_path):
241
- id = flask.g.UUID
242
- os.remove(f"{folder_absolute_path}/{str(id)}.txt")
243
-
244
-
245
- def set_interval(func, sec):
246
- def func_wrapper():
247
- set_interval(func, sec)
248
- func()
249
-
250
- t = threading.Timer(sec, func_wrapper)
251
- t.daemon = True
252
- t.start()
253
- return t
254
-
255
-
256
- def extension_from_filename(filename):
257
- return os.path.splitext(filename)[1][1:]
258
-
259
-
260
- def validate_request(request, schema):
261
- json_data = request.get_json(force=True, silent=True)
262
-
263
- if json_data is None:
264
- json_data = {}
265
-
266
- try:
267
- validate(instance=json_data, schema=schema)
268
- except ValidationError as e:
269
- flask.abort(400, f"Validation error: {e.message}")
270
-
271
-
272
199
  def geographic_coordinate_systems(geode_object: str):
273
200
  if is_3D(geode_object):
274
201
  return og_gs.GeographicCoordinateSystem3D.geographic_coordinate_systems()
@@ -329,41 +256,3 @@ def create_coordinate_system(
329
256
  create_crs(
330
257
  geode_object, data, name, input_coordiante_system, output_coordiante_system
331
258
  )
332
-
333
-
334
- def send_file(upload_folder, saved_files, new_file_name):
335
- if len(saved_files) == 1:
336
- mimetype = "application/octet-binary"
337
- else:
338
- mimetype = "application/zip"
339
- new_file_name = os.path.splitext(new_file_name)[0] + ".zip"
340
- with zipfile.ZipFile(os.path.join(upload_folder, new_file_name), "w") as zipObj:
341
- for saved_file_path in saved_files:
342
- zipObj.write(
343
- saved_file_path,
344
- os.path.basename(saved_file_path),
345
- )
346
-
347
- response = flask.send_from_directory(
348
- directory=upload_folder,
349
- path=new_file_name,
350
- as_attachment=True,
351
- mimetype=mimetype,
352
- )
353
- response.headers["new-file-name"] = new_file_name
354
- response.headers["Access-Control-Expose-Headers"] = "new-file-name"
355
-
356
- return response
357
-
358
-
359
- def handle_exception(e):
360
- response = e.get_response()
361
- response.data = flask.json.dumps(
362
- {
363
- "code": e.code,
364
- "name": e.name,
365
- "description": e.description,
366
- }
367
- )
368
- response.content_type = "application/json"
369
- return response
@@ -1,17 +1,28 @@
1
1
  # Standard library imports
2
2
  import json
3
3
  import os
4
+ import time
4
5
 
5
6
  # Third party imports
6
7
  import flask
7
- import flask_cors
8
- from .. import geode_functions
8
+ from .. import geode_functions, utils_functions
9
9
  import werkzeug
10
10
  import uuid
11
11
 
12
-
13
12
  routes = flask.Blueprint("routes", __name__)
14
- flask_cors.CORS(routes)
13
+
14
+
15
+ @routes.before_request
16
+ def before_request():
17
+ if "ping" not in flask.request.path:
18
+ utils_functions.increment_request_counter(flask.current_app)
19
+
20
+
21
+ @routes.teardown_request
22
+ def teardown_request(exception):
23
+ if "ping" not in flask.request.path:
24
+ utils_functions.decrement_request_counter(flask.current_app)
25
+ utils_functions.update_last_request_time(flask.current_app)
15
26
 
16
27
 
17
28
  schemas = os.path.join(os.path.dirname(__file__), "schemas")
@@ -28,7 +39,7 @@ with open(
28
39
  methods=allowed_files_json["methods"],
29
40
  )
30
41
  def allowed_files():
31
- geode_functions.validate_request(flask.request, allowed_files_json)
42
+ utils_functions.validate_request(flask.request, allowed_files_json)
32
43
  extensions = geode_functions.list_input_extensions(
33
44
  flask.request.json["supported_feature"]
34
45
  )
@@ -75,7 +86,7 @@ def allowed_objects():
75
86
  return flask.make_response({}, 200)
76
87
 
77
88
  UPLOAD_FOLDER = flask.current_app.config["UPLOAD_FOLDER"]
78
- geode_functions.validate_request(flask.request, allowed_objects_json)
89
+ utils_functions.validate_request(flask.request, allowed_objects_json)
79
90
  file_absolute_path = os.path.join(UPLOAD_FOLDER, flask.request.json["filename"])
80
91
  allowed_objects = geode_functions.list_geode_objects(
81
92
  file_absolute_path, flask.request.json["supported_feature"]
@@ -96,7 +107,7 @@ with open(
96
107
  )
97
108
  def missing_files():
98
109
  UPLOAD_FOLDER = flask.current_app.config["UPLOAD_FOLDER"]
99
- geode_functions.validate_request(flask.request, missing_files_json)
110
+ utils_functions.validate_request(flask.request, missing_files_json)
100
111
 
101
112
  missing_files = geode_functions.missing_files(
102
113
  flask.request.json["input_geode_object"],
@@ -134,13 +145,11 @@ with open(
134
145
  methods=geographic_coordinate_systems_json["methods"],
135
146
  )
136
147
  def crs_converter_geographic_coordinate_systems():
137
- geode_functions.validate_request(flask.request, geographic_coordinate_systems_json)
148
+ utils_functions.validate_request(flask.request, geographic_coordinate_systems_json)
138
149
  infos = geode_functions.geographic_coordinate_systems(
139
150
  flask.request.json["input_geode_object"]
140
151
  )
141
152
  crs_list = []
142
- print(infos)
143
- print(flask.request.json["input_geode_object"])
144
153
  for info in infos:
145
154
  crs = {}
146
155
  crs["name"] = info.name
@@ -164,7 +173,7 @@ with open(
164
173
  )
165
174
  def inspect_file():
166
175
  UPLOAD_FOLDER = flask.current_app.config["UPLOAD_FOLDER"]
167
- geode_functions.validate_request(flask.request, inspect_file_json)
176
+ utils_functions.validate_request(flask.request, inspect_file_json)
168
177
 
169
178
  secure_filename = werkzeug.utils.secure_filename(flask.request.json["filename"])
170
179
  file_path = os.path.abspath(os.path.join(UPLOAD_FOLDER, secure_filename))
@@ -189,7 +198,7 @@ with open(
189
198
  )
190
199
  def geode_objects_and_output_extensions():
191
200
  UPLOAD_FOLDER = flask.current_app.config["UPLOAD_FOLDER"]
192
- geode_functions.validate_request(
201
+ utils_functions.validate_request(
193
202
  flask.request, geode_objects_and_output_extensions_json
194
203
  )
195
204
  data = geode_functions.load(
@@ -221,7 +230,7 @@ with open(
221
230
  def save_viewable_file():
222
231
  UPLOAD_FOLDER = flask.current_app.config["UPLOAD_FOLDER"]
223
232
  DATA_FOLDER_PATH = flask.current_app.config["DATA_FOLDER_PATH"]
224
- geode_functions.validate_request(flask.request, save_viewable_file_json)
233
+ utils_functions.validate_request(flask.request, save_viewable_file_json)
225
234
 
226
235
  secure_filename = werkzeug.utils.secure_filename(flask.request.json["filename"])
227
236
  file_path = os.path.abspath(os.path.join(UPLOAD_FOLDER, secure_filename))
@@ -260,3 +269,20 @@ def save_viewable_file():
260
269
  },
261
270
  200,
262
271
  )
272
+
273
+
274
+ with open(
275
+ os.path.join(schemas, "ping.json"),
276
+ "r",
277
+ ) as file:
278
+ ping_json = json.load(file)
279
+
280
+
281
+ @routes.route(
282
+ ping_json["route"],
283
+ methods=ping_json["methods"],
284
+ )
285
+ def ping():
286
+ utils_functions.validate_request(flask.request, ping_json)
287
+ flask.current_app.config.update(LAST_PING_TIME=time.time())
288
+ return flask.make_response({"message": "Flask server is running"}, 200)
@@ -0,0 +1,10 @@
1
+ {
2
+ "route": "/ping",
3
+ "methods": [
4
+ "POST"
5
+ ],
6
+ "type": "object",
7
+ "properties": {},
8
+ "required": [],
9
+ "additionalProperties": false
10
+ }
@@ -0,0 +1,131 @@
1
+ # Standard library imports
2
+ import os
3
+ import threading
4
+ import time
5
+ import zipfile
6
+
7
+ # Third party imports
8
+ import flask
9
+ from jsonschema import validate
10
+ from jsonschema.exceptions import ValidationError
11
+ import pkg_resources
12
+
13
+ # Local application imports
14
+
15
+
16
+ def increment_request_counter(current_app):
17
+ if "REQUEST_COUNTER" in current_app.config:
18
+ REQUEST_COUNTER = int(current_app.config.get("REQUEST_COUNTER"))
19
+ REQUEST_COUNTER += 1
20
+ current_app.config.update(REQUEST_COUNTER=REQUEST_COUNTER)
21
+
22
+
23
+ def decrement_request_counter(current_app):
24
+ if "REQUEST_COUNTER" in current_app.config:
25
+ REQUEST_COUNTER = int(current_app.config.get("REQUEST_COUNTER"))
26
+ REQUEST_COUNTER -= 1
27
+ current_app.config.update(REQUEST_COUNTER=REQUEST_COUNTER)
28
+
29
+
30
+ def update_last_request_time(current_app):
31
+ if "LAST_REQUEST_TIME" in current_app.config:
32
+ LAST_REQUEST_TIME = time.time()
33
+ current_app.config.update(LAST_REQUEST_TIME=LAST_REQUEST_TIME)
34
+
35
+
36
+ def kill_task(current_app):
37
+ DESKTOP_APP = bool(current_app.config.get("DESKTOP_APP"))
38
+ REQUEST_COUNTER = int(current_app.config.get("REQUEST_COUNTER"))
39
+ LAST_PING_TIME = float(current_app.config.get("LAST_PING_TIME"))
40
+ LAST_REQUEST_TIME = float(current_app.config.get("LAST_REQUEST_TIME"))
41
+ MINUTES_BEFORE_TIMEOUT = float(current_app.config.get("MINUTES_BEFORE_TIMEOUT"))
42
+ current_time = time.time()
43
+ minutes_since_last_request = (current_time - LAST_REQUEST_TIME) / 60
44
+ minutes_since_last_ping = (current_time - LAST_PING_TIME) / 60
45
+
46
+ if (
47
+ (
48
+ (minutes_since_last_request > MINUTES_BEFORE_TIMEOUT)
49
+ and (DESKTOP_APP == False)
50
+ )
51
+ or (minutes_since_last_ping > MINUTES_BEFORE_TIMEOUT)
52
+ ) and (REQUEST_COUNTER == 0):
53
+ print("Server timed out due to inactivity, shutting down...", flush=True)
54
+ os._exit(0)
55
+
56
+
57
+ def versions(list_packages: list):
58
+ list_with_versions = []
59
+ for package in list_packages:
60
+ list_with_versions.append(
61
+ {
62
+ "package": package,
63
+ "version": pkg_resources.get_distribution(package).version,
64
+ }
65
+ )
66
+ return list_with_versions
67
+
68
+
69
+ def validate_request(request, schema):
70
+ json_data = request.get_json(force=True, silent=True)
71
+
72
+ if json_data is None:
73
+ json_data = {}
74
+
75
+ try:
76
+ validate(instance=json_data, schema=schema)
77
+ except ValidationError as e:
78
+ flask.abort(400, f"Validation error: {e.message}")
79
+
80
+
81
+ def set_interval(func, sec, args=None):
82
+ def func_wrapper():
83
+ set_interval(func, sec, args)
84
+ func(args)
85
+
86
+ t = threading.Timer(sec, func_wrapper)
87
+ t.daemon = True
88
+ t.start()
89
+ return t
90
+
91
+
92
+ def extension_from_filename(filename):
93
+ return os.path.splitext(filename)[1][1:]
94
+
95
+
96
+ def send_file(upload_folder, saved_files, new_file_name):
97
+ if len(saved_files) == 1:
98
+ mimetype = "application/octet-binary"
99
+ else:
100
+ mimetype = "application/zip"
101
+ new_file_name = os.path.splitext(new_file_name)[0] + ".zip"
102
+ with zipfile.ZipFile(os.path.join(upload_folder, new_file_name), "w") as zipObj:
103
+ for saved_file_path in saved_files:
104
+ zipObj.write(
105
+ saved_file_path,
106
+ os.path.basename(saved_file_path),
107
+ )
108
+
109
+ response = flask.send_from_directory(
110
+ directory=upload_folder,
111
+ path=new_file_name,
112
+ as_attachment=True,
113
+ mimetype=mimetype,
114
+ )
115
+ response.headers["new-file-name"] = new_file_name
116
+ response.headers["Access-Control-Expose-Headers"] = "new-file-name"
117
+
118
+ return response
119
+
120
+
121
+ def handle_exception(e):
122
+ response = e.get_response()
123
+ response.data = flask.json.dumps(
124
+ {
125
+ "code": e.code,
126
+ "name": e.name,
127
+ "description": e.description,
128
+ }
129
+ )
130
+ response.content_type = "application/json"
131
+ return response