OpenGeodeWeb-Back 6.12.5rc2__tar.gz → 6.13.0__tar.gz

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.
Files changed (114) hide show
  1. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/PKG-INFO +1 -1
  2. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/pyproject.toml +1 -1
  3. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/PKG-INFO +1 -1
  4. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/blueprint_routes.py +96 -33
  5. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/tests/test_routes.py +152 -7
  6. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/LICENSE +0 -0
  7. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/README.md +0 -0
  8. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/requirements.txt +0 -0
  9. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/setup.cfg +0 -0
  10. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/SOURCES.txt +0 -0
  11. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/dependency_links.txt +0 -0
  12. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/entry_points.txt +0 -0
  13. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/requires.txt +0 -0
  14. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/OpenGeodeWeb_Back.egg-info/top_level.txt +0 -0
  15. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/__init__.py +0 -0
  16. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/app.py +0 -0
  17. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/app_config.py +0 -0
  18. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_functions.py +0 -0
  19. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/__init__.py +0 -0
  20. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_brep.py +0 -0
  21. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_cross_section.py +0 -0
  22. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_edged_curve2d.py +0 -0
  23. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_edged_curve3d.py +0 -0
  24. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_graph.py +0 -0
  25. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_grid2d.py +0 -0
  26. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_grid3d.py +0 -0
  27. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py +0 -0
  28. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_hybrid_solid3d.py +0 -0
  29. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_implicit_cross_section.py +0 -0
  30. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_implicit_structural_model.py +0 -0
  31. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid2d.py +0 -0
  32. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_light_regular_grid3d.py +0 -0
  33. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_mesh.py +0 -0
  34. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_model.py +0 -0
  35. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_object.py +0 -0
  36. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_point_set2d.py +0 -0
  37. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_point_set3d.py +0 -0
  38. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface2d.py +0 -0
  39. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_polygonal_surface3d.py +0 -0
  40. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_polyhedral_solid3d.py +0 -0
  41. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_raster_image2d.py +0 -0
  42. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_raster_image3d.py +0 -0
  43. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_regular_grid2d.py +0 -0
  44. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_regular_grid3d.py +0 -0
  45. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_section.py +0 -0
  46. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_solid_mesh3d.py +0 -0
  47. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_structural_model.py +0 -0
  48. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_surface_mesh2d.py +0 -0
  49. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_surface_mesh3d.py +0 -0
  50. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_tetrahedral_solid3d.py +0 -0
  51. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface2d.py +0 -0
  52. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_triangulated_surface3d.py +0 -0
  53. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/geode_objects/geode_vertex_set.py +0 -0
  54. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/py.typed +0 -0
  55. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/blueprint_create.py +0 -0
  56. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/__init__.py +0 -0
  57. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/edged_curve.json +0 -0
  58. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/edged_curve.py +0 -0
  59. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/point_set.json +0 -0
  60. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/point_set.py +0 -0
  61. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.json +0 -0
  62. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/create/schemas/polygonal_surface.py +0 -0
  63. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/__init__.py +0 -0
  64. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/allowed_files.json +0 -0
  65. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/allowed_files.py +0 -0
  66. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/allowed_objects.json +0 -0
  67. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/allowed_objects.py +0 -0
  68. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json +0 -0
  69. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py +0 -0
  70. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json +0 -0
  71. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py +0 -0
  72. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/export_project.json +0 -0
  73. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/export_project.py +0 -0
  74. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.json +0 -0
  75. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geode_object_inheritance.py +0 -0
  76. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.json +0 -0
  77. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geode_objects_and_output_extensions.py +0 -0
  78. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geographic_coordinate_systems.json +0 -0
  79. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/geographic_coordinate_systems.py +0 -0
  80. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/import_project.json +0 -0
  81. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/import_project.py +0 -0
  82. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/kill.json +0 -0
  83. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/kill.py +0 -0
  84. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/missing_files.json +0 -0
  85. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/missing_files.py +0 -0
  86. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json +0 -0
  87. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py +0 -0
  88. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json +0 -0
  89. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py +0 -0
  90. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json +0 -0
  91. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py +0 -0
  92. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json +0 -0
  93. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py +0 -0
  94. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/ping.json +0 -0
  95. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/ping.py +0 -0
  96. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json +0 -0
  97. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py +0 -0
  98. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json +0 -0
  99. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py +0 -0
  100. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/save_viewable_file.json +0 -0
  101. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/save_viewable_file.py +0 -0
  102. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/texture_coordinates.json +0 -0
  103. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/texture_coordinates.py +0 -0
  104. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/upload_file.json +0 -0
  105. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/upload_file.py +0 -0
  106. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/validate.json +0 -0
  107. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/validate.py +0 -0
  108. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json +0 -0
  109. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py +0 -0
  110. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/test_utils.py +0 -0
  111. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/src/opengeodeweb_back/utils_functions.py +0 -0
  112. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/tests/test_create_routes.py +0 -0
  113. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/tests/test_geode_functions.py +0 -0
  114. {opengeodeweb_back-6.12.5rc2 → opengeodeweb_back-6.13.0}/tests/test_utils_functions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenGeodeWeb-Back
3
- Version: 6.12.5rc2
3
+ Version: 6.13.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OpenGeodeWeb-Back"
7
- version = "6.12.5-rc.2"
7
+ version = "6.13.0"
8
8
  dynamic = ["dependencies"]
9
9
  authors = [{ name = "Geode-solutions", email = "team-web@geode-solutions.com" }]
10
10
  description = "OpenGeodeWeb-Back is an open source framework that proposes handy python functions and wrappers for the OpenGeode ecosystem"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenGeodeWeb-Back
3
- Version: 6.12.5rc2
3
+ Version: 6.13.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
@@ -4,6 +4,7 @@ import os
4
4
  import time
5
5
  import shutil
6
6
  import math
7
+ import typing
7
8
  from threading import Timer
8
9
 
9
10
  # Third party imports
@@ -63,6 +64,18 @@ def allowed_files() -> flask.Response:
63
64
  return flask.make_response({"extensions": list(extensions)}, 200)
64
65
 
65
66
 
67
+ def _write_stream(path: str, stream: typing.IO[bytes], mode: str = "wb") -> None:
68
+ read_buffer_size = 1024 * 1024
69
+ with open(path, mode) as destination:
70
+ while chunk := stream.read(read_buffer_size):
71
+ destination.write(chunk)
72
+
73
+
74
+ def _finalize_upload(filename: str) -> flask.Response:
75
+ print(f"{filename=}", flush=True)
76
+ return flask.make_response({"message": "File uploaded"}, 201)
77
+
78
+
66
79
  @routes.route(
67
80
  schemas_dict["upload_file"]["route"],
68
81
  methods=schemas_dict["upload_file"]["methods"],
@@ -73,18 +86,43 @@ def upload_file() -> flask.Response:
73
86
  if not os.path.exists(UPLOAD_FOLDER_PATH):
74
87
  os.makedirs(UPLOAD_FOLDER_PATH, exist_ok=True)
75
88
 
76
- file = flask.request.files["file"]
77
- if file.filename is None:
89
+ # Multipart callers (e.g. Vease) still send the whole file as a "file" form
90
+ # part. Everything else PUTs raw bytes with ?filename= as a query param:
91
+ # either the whole file in one request, or one of several chunks (when
92
+ # ?chunk_index=/?total_chunks= are also present) that get appended in
93
+ # order and assembled into the final file once the last one arrives. This
94
+ # keeps every request under cloud hosting's hard request-size limit.
95
+ if flask.request.mimetype == "multipart/form-data":
96
+ file = flask.request.files["file"]
97
+ if file.filename is None:
98
+ flask.abort(400, "Filename is required")
99
+ filename = werkzeug.utils.secure_filename(os.path.basename(file.filename))
100
+ file_path = os.path.join(UPLOAD_FOLDER_PATH, filename)
101
+ file.save(file_path)
102
+ return _finalize_upload(filename)
103
+
104
+ raw_filename = flask.request.args.get("filename")
105
+ if not raw_filename:
78
106
  flask.abort(400, "Filename is required")
79
- filename = werkzeug.utils.secure_filename(os.path.basename(file.filename))
80
- print(f"{filename=}", flush=True)
107
+ filename = werkzeug.utils.secure_filename(os.path.basename(raw_filename))
81
108
  file_path = os.path.join(UPLOAD_FOLDER_PATH, filename)
82
- file.save(file_path)
83
- if filename.lower().endswith(".csv.json"):
84
- shutil.copyfile(
85
- file_path, os.path.join(UPLOAD_FOLDER_PATH, filename[:-9] + ".json")
86
- )
87
- return flask.make_response({"message": "File uploaded"}, 201)
109
+
110
+ total_chunks = flask.request.args.get("total_chunks", type=int)
111
+ if total_chunks is None:
112
+ _write_stream(file_path, flask.request.stream)
113
+ return _finalize_upload(filename)
114
+
115
+ chunk_index = flask.request.args.get("chunk_index", type=int)
116
+ if chunk_index is None or not 0 <= chunk_index < total_chunks:
117
+ flask.abort(400, "Invalid chunk_index")
118
+
119
+ part_path = f"{file_path}.part"
120
+ _write_stream(part_path, flask.request.stream, "wb" if chunk_index == 0 else "ab")
121
+ if chunk_index < total_chunks - 1:
122
+ return flask.make_response({"message": "Chunk received"}, 200)
123
+
124
+ os.replace(part_path, file_path)
125
+ return _finalize_upload(filename)
88
126
 
89
127
 
90
128
  @routes.route(
@@ -256,32 +294,54 @@ def extract_valid_attribute_values(
256
294
  attribute_manager: og.AttributeManager,
257
295
  attribute_name: str,
258
296
  item_index: int,
259
- ) -> list[float]:
260
- attribute_ids_matching_name = attribute_manager.attribute_ids_matching_name(
261
- attribute_name
262
- )
263
- if not isinstance(attribute_ids_matching_name, list):
264
- return []
265
- component_attribute = attribute_manager.find_generic_attribute(
266
- attribute_ids_matching_name[0]
267
- )
268
- if component_attribute is None:
269
- return []
270
- if not component_attribute.is_genericable():
271
- return []
297
+ ) -> tuple[list[float], bool]:
298
+ attribute_ids = attribute_manager.attribute_ids_matching_name(attribute_name)
299
+ if not isinstance(attribute_ids, list):
300
+ return [], False
301
+ attribute = attribute_manager.find_generic_attribute(attribute_ids[0])
302
+ if (
303
+ attribute is None
304
+ or not attribute.is_genericable()
305
+ or not attribute.properties().transferable
306
+ ):
307
+ return [], False
308
+
309
+ nb_items = attribute.nb_items()
310
+ default_values = getattr(attribute, "default_values", None)
311
+ no_value = default_values().no_value if default_values else None
312
+ if no_value is None and (
313
+ "Point" in attribute.type() or "Vector" in attribute.type()
314
+ ):
315
+ no_value = [0.0] * nb_items
316
+
317
+ value_getter = getattr(
318
+ attribute,
319
+ "value",
320
+ lambda element_index: [
321
+ attribute.generic_item_value(element_index, i) for i in range(nb_items)
322
+ ],
323
+ )
324
+
272
325
  valid_values: list[float] = []
326
+ has_nan = False
273
327
  for element_index in range(attribute_manager.nb_elements()):
274
- value = component_attribute.generic_item_value(element_index, item_index)
275
- if value is not None and not math.isnan(value):
328
+ value = attribute.generic_item_value(element_index, item_index)
329
+ if (
330
+ value is None
331
+ or math.isnan(value)
332
+ or (no_value is not None and value_getter(element_index) == no_value)
333
+ ):
334
+ has_nan = True
335
+ else:
276
336
  valid_values.append(value)
277
- return valid_values
337
+ return valid_values, has_nan
278
338
 
279
339
 
280
340
  def attributes_metadata(
281
341
  manager: og.AttributeManager | list[og.AttributeManager],
282
- ) -> list[dict[str, str | int | float | list[float]]]:
342
+ ) -> list[dict[str, str | int | float | bool | list[float]]]:
283
343
  attribute_managers = manager if isinstance(manager, list) else [manager]
284
- attributes: list[dict[str, str | int | float | list[float]]] = []
344
+ attributes: list[dict[str, str | int | float | bool | list[float]]] = []
285
345
  first_manager = attribute_managers[0]
286
346
  for id in first_manager.attribute_ids():
287
347
  attribute = first_manager.find_generic_attribute(id)
@@ -290,18 +350,20 @@ def attributes_metadata(
290
350
  attribute_name = attribute.name()
291
351
  if attribute_name is None:
292
352
  continue
293
- if not attribute.is_genericable():
353
+ if not attribute.is_genericable() or not attribute.properties().transferable:
294
354
  continue
295
355
  nb_items = attribute.nb_items()
296
356
  min_values, max_values = [], []
357
+ attribute_has_nan = False
297
358
  for item_index in range(nb_items):
298
359
  valid_values: list[float] = []
299
360
  for attribute_manager in attribute_managers:
300
- valid_values.extend(
301
- extract_valid_attribute_values(
302
- attribute_manager, attribute_name, item_index
303
- )
361
+ extracted_values, has_nan = extract_valid_attribute_values(
362
+ attribute_manager, attribute_name, item_index
304
363
  )
364
+ valid_values.extend(extracted_values)
365
+ if has_nan:
366
+ attribute_has_nan = True
305
367
  if valid_values:
306
368
  min_values.append(min(valid_values))
307
369
  max_values.append(max(valid_values))
@@ -316,6 +378,7 @@ def attributes_metadata(
316
378
  "max_value": max(max_values),
317
379
  "min_values": min_values,
318
380
  "max_values": max_values,
381
+ "no_data": attribute_has_nan,
319
382
  }
320
383
  )
321
384
  return attributes
@@ -2,6 +2,7 @@
2
2
  import os
3
3
 
4
4
  # Third party imports
5
+ import opengeode as og
5
6
  from werkzeug.datastructures import FileStorage
6
7
  from flask.testing import FlaskClient
7
8
  from werkzeug.test import TestResponse
@@ -13,6 +14,7 @@ import zipfile
13
14
  from opengeodeweb_microservice.database.data import Data
14
15
  from opengeodeweb_microservice.database.connection import get_session
15
16
  from opengeodeweb_back import geode_functions, test_utils
17
+ from opengeodeweb_back.routes.blueprint_routes import extract_valid_attribute_values
16
18
  from opengeodeweb_back.geode_objects.geode_polygonal_surface3d import (
17
19
  GeodePolygonalSurface3D,
18
20
  )
@@ -79,6 +81,90 @@ def test_upload_file(client: FlaskClient, filename: str = "test.og_brep") -> Non
79
81
  assert response.status_code == 201
80
82
 
81
83
 
84
+ def test_upload_file_raw(client: FlaskClient, filename: str = "test.og_brep") -> None:
85
+ file = os.path.join(data_dir, filename)
86
+ with open(file, "rb") as opened_file:
87
+ file_bytes = opened_file.read()
88
+
89
+ raw_filename = "raw_upload_test.og_brep"
90
+ response = client.put(
91
+ f"/opengeodeweb_back/upload_file?filename={raw_filename}",
92
+ data=file_bytes,
93
+ content_type="application/octet-stream",
94
+ )
95
+ assert response.status_code == 201
96
+
97
+ uploaded_path = os.path.join(data_dir, raw_filename)
98
+ try:
99
+ with open(uploaded_path, "rb") as uploaded_file:
100
+ assert uploaded_file.read() == file_bytes
101
+ finally:
102
+ os.remove(uploaded_path)
103
+
104
+
105
+ def test_upload_file_raw_missing_filename(client: FlaskClient) -> None:
106
+ response = client.put(
107
+ f"/opengeodeweb_back/upload_file",
108
+ data=b"some raw bytes",
109
+ content_type="application/octet-stream",
110
+ )
111
+ assert response.status_code == 400
112
+
113
+
114
+ def test_upload_file_chunked(
115
+ client: FlaskClient, filename: str = "test.og_brep"
116
+ ) -> None:
117
+ file = os.path.join(data_dir, filename)
118
+ with open(file, "rb") as opened_file:
119
+ file_bytes = opened_file.read()
120
+
121
+ chunk_filename = "chunked_upload_test.og_brep"
122
+ chunk_size = max(1, len(file_bytes) // 3)
123
+ chunks = [
124
+ file_bytes[index : index + chunk_size]
125
+ for index in range(0, len(file_bytes), chunk_size)
126
+ ]
127
+ total_chunks = len(chunks)
128
+
129
+ uploaded_path = os.path.join(data_dir, chunk_filename)
130
+ try:
131
+ for chunk_index, chunk in enumerate(chunks):
132
+ response = client.put(
133
+ f"/opengeodeweb_back/upload_file"
134
+ f"?filename={chunk_filename}"
135
+ f"&chunk_index={chunk_index}"
136
+ f"&total_chunks={total_chunks}",
137
+ data=chunk,
138
+ content_type="application/octet-stream",
139
+ )
140
+ if chunk_index < total_chunks - 1:
141
+ assert response.status_code == 200
142
+ assert not os.path.exists(uploaded_path)
143
+ else:
144
+ assert response.status_code == 201
145
+
146
+ with open(uploaded_path, "rb") as uploaded_file:
147
+ assert uploaded_file.read() == file_bytes
148
+ finally:
149
+ if os.path.exists(uploaded_path):
150
+ os.remove(uploaded_path)
151
+ part_path = f"{uploaded_path}.part"
152
+ if os.path.exists(part_path):
153
+ os.remove(part_path)
154
+
155
+
156
+ def test_upload_file_chunked_invalid_chunk_index(client: FlaskClient) -> None:
157
+ response = client.put(
158
+ "/opengeodeweb_back/upload_file"
159
+ "?filename=invalid_chunk_index.og_brep"
160
+ "&chunk_index=2"
161
+ "&total_chunks=2",
162
+ data=b"some raw bytes",
163
+ content_type="application/octet-stream",
164
+ )
165
+ assert response.status_code == 400
166
+
167
+
82
168
  def test_missing_files(client: FlaskClient) -> None:
83
169
  route = f"/opengeodeweb_back/missing_files"
84
170
 
@@ -261,6 +347,8 @@ def test_vertex_attribute_names(client: FlaskClient, test_id: str) -> None:
261
347
  assert "nb_items" in attribute
262
348
  assert "min_values" in attribute
263
349
  assert "max_values" in attribute
350
+ assert "no_data" in attribute
351
+ assert isinstance(attribute["no_data"], bool)
264
352
  print(
265
353
  f"[ATTRIBUTES]: ",
266
354
  [attribute["nb_items"] for attribute in attributes],
@@ -297,6 +385,8 @@ def test_cell_attribute_names(client: FlaskClient, test_id: str) -> None:
297
385
  assert "nb_items" in attribute
298
386
  assert "min_values" in attribute
299
387
  assert "max_values" in attribute
388
+ assert "no_data" in attribute
389
+ assert isinstance(attribute["no_data"], bool)
300
390
  print(
301
391
  f"[ATTRIBUTES]: ",
302
392
  [attribute["nb_items"] for attribute in attributes],
@@ -333,6 +423,8 @@ def test_polygon_attribute_names(client: FlaskClient, test_id: str) -> None:
333
423
  assert "nb_items" in attribute
334
424
  assert "min_values" in attribute
335
425
  assert "max_values" in attribute
426
+ assert "no_data" in attribute
427
+ assert isinstance(attribute["no_data"], bool)
336
428
  print(
337
429
  f"[ATTRIBUTES]: ",
338
430
  [attribute["nb_items"] for attribute in attributes],
@@ -370,6 +462,8 @@ def test_polyhedron_attribute_names(client: FlaskClient, test_id: str) -> None:
370
462
  assert "nb_items" in attribute
371
463
  assert "min_values" in attribute
372
464
  assert "max_values" in attribute
465
+ assert "no_data" in attribute
466
+ assert isinstance(attribute["no_data"], bool)
373
467
  if attribute["attribute_name"] == "Range":
374
468
  assert attribute["min_value"] == 0.0
375
469
  assert attribute["max_value"] == 579.0
@@ -411,6 +505,8 @@ def test_edge_attribute_names(client: FlaskClient, test_id: str) -> None:
411
505
  assert "nb_items" in attribute
412
506
  assert "min_values" in attribute
413
507
  assert "max_values" in attribute
508
+ assert "no_data" in attribute
509
+ assert isinstance(attribute["no_data"], bool)
414
510
  print(
415
511
  f"[ATTRIBUTES]: ",
416
512
  [attribute["nb_items"] for attribute in attributes],
@@ -485,7 +581,7 @@ def test_model_components(client: FlaskClient) -> None:
485
581
  assert isinstance(mesh_components, list)
486
582
  assert len(mesh_components) > 0
487
583
  for mesh_component in mesh_components:
488
- assert isinstance(mesh_component, object)
584
+ assert isinstance(mesh_component, dict)
489
585
  assert isinstance(mesh_component["geode_id"], str)
490
586
  assert isinstance(mesh_component["viewer_id"], int)
491
587
  assert isinstance(mesh_component["name"], str)
@@ -501,7 +597,7 @@ def test_model_components(client: FlaskClient) -> None:
501
597
  collection_components = response.get_json()["collection_components"]
502
598
  assert isinstance(collection_components, list)
503
599
  for collection_component in collection_components:
504
- assert isinstance(collection_component, object)
600
+ assert isinstance(collection_component, dict)
505
601
  assert isinstance(collection_component["geode_id"], str)
506
602
  assert isinstance(collection_component["name"], str)
507
603
  assert isinstance(collection_component["items"], list)
@@ -649,15 +745,18 @@ def _load_brep_components(client: FlaskClient) -> tuple[str, dict[str, list[str]
649
745
  response = test_save_viewable_file(client, "BRep", "cube.og_brep")
650
746
  assert response.status_code == 200
651
747
  model_id: str = response.get_json()["id"]
652
- mesh_components: list[dict] = response.get_json()["mesh_components"]
748
+ mesh_components: list[dict[str, object]] = response.get_json()["mesh_components"]
653
749
  by_type: dict[str, list[str]] = {}
654
- for mc in mesh_components:
655
- component_type = mc["type"]
656
- by_type.setdefault(component_type, []).append(mc["geode_id"])
750
+ for mesh_component in mesh_components:
751
+ component_type = mesh_component["type"]
752
+ geode_id = mesh_component["geode_id"]
753
+ assert isinstance(component_type, str)
754
+ assert isinstance(geode_id, str)
755
+ by_type.setdefault(component_type, []).append(geode_id)
657
756
  return model_id, by_type
658
757
 
659
758
 
660
- def _assert_attributes_response(response) -> None:
759
+ def _assert_attributes_response(response: TestResponse) -> None:
661
760
  assert response.status_code == 200
662
761
  attributes = response.get_json()["attributes"]
663
762
  assert isinstance(attributes, list)
@@ -668,6 +767,8 @@ def _assert_attributes_response(response) -> None:
668
767
  assert "nb_items" in attribute
669
768
  assert "min_values" in attribute
670
769
  assert "max_values" in attribute
770
+ assert "no_data" in attribute
771
+ assert isinstance(attribute["no_data"], bool)
671
772
 
672
773
 
673
774
  def test_model_component_vertex_attribute_names(client: FlaskClient) -> None:
@@ -732,3 +833,47 @@ def test_model_component_polyhedron_attribute_names(client: FlaskClient) -> None
732
833
  return {"id": model_id, "component_ids": block_ids}
733
834
 
734
835
  test_utils.test_route_wrong_params(client, route, get_full_data)
836
+
837
+
838
+ def test_extract_valid_attribute_values_with_sentinel_no_value() -> None:
839
+ mesh = og.TriangulatedSurface3D.create()
840
+ builder = og.TriangulatedSurfaceBuilder3D.create(mesh)
841
+ vertex_0 = builder.create_point(og.Point3D([0, 0, 0]))
842
+ vertex_1 = builder.create_point(og.Point3D([1, 0, 0]))
843
+ builder.create_triangle([vertex_0, vertex_1, vertex_0])
844
+
845
+ attribute_manager = mesh.vertex_attribute_manager()
846
+ values_config = og.AttributeValuesDouble()
847
+ values_config.no_value = -999.0
848
+ attribute_id = attribute_manager.create_attribute_variable_double(
849
+ "variable_double", values_config, og.AttributeProperties()
850
+ )
851
+ attribute = attribute_manager.find_attribute_variable_double(attribute_id)
852
+ attribute.set_value(vertex_0, -999.0)
853
+ attribute.set_value(vertex_1, 42.0)
854
+
855
+ valid_values, has_nan = extract_valid_attribute_values(
856
+ attribute_manager, "variable_double", 0
857
+ )
858
+ assert has_nan is True
859
+ assert valid_values == [42.0]
860
+
861
+
862
+ def test_extract_valid_attribute_values_with_non_transferable_attribute() -> None:
863
+ mesh = og.TriangulatedSurface3D.create()
864
+ builder = og.TriangulatedSurfaceBuilder3D.create(mesh)
865
+ vertex_0 = builder.create_point(og.Point3D([0, 0, 0]))
866
+ builder.create_triangle([vertex_0, vertex_0, vertex_0])
867
+
868
+ attribute_manager = mesh.vertex_attribute_manager()
869
+ properties = og.AttributeProperties()
870
+ properties.transferable = False
871
+ attribute_manager.create_attribute_variable_double(
872
+ "internal_attribute", og.AttributeValuesDouble(), properties
873
+ )
874
+
875
+ valid_values, has_nan = extract_valid_attribute_values(
876
+ attribute_manager, "internal_attribute", 0
877
+ )
878
+ assert has_nan is False
879
+ assert valid_values == []