first commit
This commit is contained in:
0
helix/validators/__init__.py
Normal file
0
helix/validators/__init__.py
Normal file
92
helix/validators/csv_input_validator.py
Normal file
92
helix/validators/csv_input_validator.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import csv
|
||||
from helix.constants.file_validation_error import FileValidationError, FileValidationMessage
|
||||
|
||||
|
||||
class CsvInputValidator(object):
|
||||
|
||||
def __init__(self, user_values):
|
||||
self.user_values = user_values
|
||||
|
||||
def validate(self, csv_input):
|
||||
try:
|
||||
headers, rows = self.parse_cad_input(csv_input)
|
||||
|
||||
if len(headers) < 5:
|
||||
return FileValidationError(FileValidationMessage.InvalidHeaders, 0)
|
||||
if len(rows) == 0:
|
||||
return FileValidationError(FileValidationMessage.InvalidRowCount, 0)
|
||||
|
||||
for row_index, row in enumerate(rows):
|
||||
chain = [
|
||||
CsvInputValidator.validate_for_csv,
|
||||
CsvInputValidator.validate_for_wind_zones,
|
||||
CsvInputValidator.validate_for_panel_type,
|
||||
]
|
||||
result = self.run_validation_chain(headers, row, chain)
|
||||
if result:
|
||||
return FileValidationError(result, row_index + 1)
|
||||
|
||||
file_validation_chain = [
|
||||
CsvInputValidator.validate_file_for_panel_types,
|
||||
]
|
||||
result = self.run_validation_chain(headers, rows, file_validation_chain)
|
||||
if result:
|
||||
return FileValidationError(result, None)
|
||||
|
||||
return None
|
||||
except:
|
||||
return FileValidationError(FileValidationMessage.Generic, None)
|
||||
|
||||
# Row Validators
|
||||
|
||||
def validate_for_csv(self, headers, row):
|
||||
if len(row) != len(headers):
|
||||
return FileValidationMessage.Generic
|
||||
return None
|
||||
|
||||
def validate_for_wind_zones(self, headers, row):
|
||||
valid_wind_zones = self.user_values.system_type().system_constants().wind_zones
|
||||
if row[2] not in valid_wind_zones:
|
||||
return FileValidationMessage.invalid_wind_zones(self.user_values.system_type())
|
||||
return None
|
||||
|
||||
def validate_for_panel_type(self, headers, row):
|
||||
valid_panel_types = range(1, 5)
|
||||
if int(row[3]) not in valid_panel_types:
|
||||
return FileValidationMessage.PanelTypeOutOfBounds
|
||||
return None
|
||||
|
||||
# File Validators
|
||||
|
||||
def validate_file_for_panel_types(self, headers, rows):
|
||||
panel_type_counts_per_subarray = {}
|
||||
for row in rows:
|
||||
subarray = int(row[4])
|
||||
panel_type_counts = panel_type_counts_per_subarray.get(subarray) or {}
|
||||
panel_type = int(row[3])
|
||||
count = panel_type_counts.get(panel_type) or 0
|
||||
panel_type_counts[panel_type] = count + 1
|
||||
panel_type_counts_per_subarray[subarray] = panel_type_counts
|
||||
|
||||
minimum_module_count = self.user_values.system_type().system_constants().minimum_corner_module_count
|
||||
|
||||
for panel_type_counts in panel_type_counts_per_subarray.values():
|
||||
if (panel_type_counts.get(1) or 0) < minimum_module_count:
|
||||
return FileValidationMessage.panel_type_too_few_corners(self.user_values.system_type())
|
||||
return None
|
||||
|
||||
# Helpers
|
||||
|
||||
def run_validation_chain(self, headers, data, chain):
|
||||
result = None
|
||||
chain = list(chain)
|
||||
while not result and len(chain) > 0:
|
||||
method = chain.pop()
|
||||
result = method(self, headers, data)
|
||||
return result
|
||||
|
||||
def parse_cad_input(self, cad_input):
|
||||
reader = csv.reader(cad_input.splitlines(), dialect='excel-tab')
|
||||
headers = next(reader)
|
||||
rows = [row for row in reader]
|
||||
return headers, rows
|
||||
9
helix/validators/dxf_input_validator.py
Normal file
9
helix/validators/dxf_input_validator.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from helix.constants.file_validation_error import FileValidationMessage, FileValidationError
|
||||
|
||||
|
||||
class DxfInputValidator(object):
|
||||
def __init__(self, _):
|
||||
pass
|
||||
|
||||
def validate(self, dxf_input):
|
||||
return None
|
||||
65
helix/validators/dxf_layer_validator.py
Normal file
65
helix/validators/dxf_layer_validator.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Created on Mar 23, 2017
|
||||
|
||||
@author: jvazquez
|
||||
"""
|
||||
from helix.constants.file_validation_error import FileValidationMessage
|
||||
from helix.models.dxf.dxf_error import OldDxfFormatException
|
||||
|
||||
|
||||
class DXFLayerValidator(object):
|
||||
"""
|
||||
In 2017, Aurora changed the format of their DXF files to meet new
|
||||
specifications. The old-style DXF files will not
|
||||
be supported, and this class will screen for old files.
|
||||
Most noticably, old files used "Roofs" section headers while
|
||||
new files use "Buildings"
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.layers = set()
|
||||
self.new_aurora_format = False
|
||||
|
||||
def add_layer(self, layer):
|
||||
"""Add a layer in a set
|
||||
|
||||
param
|
||||
layer (string)
|
||||
|
||||
"""
|
||||
self.layers.add(layer)
|
||||
|
||||
def determine_file(self):
|
||||
"""Based on the layers of the dxf,
|
||||
we will determine if is a "new"
|
||||
file format
|
||||
or is the "old" format.
|
||||
|
||||
Bare in mind that we are <strong>assuming</strong>
|
||||
this based on an assumption.
|
||||
|
||||
There's no drastic visible difference
|
||||
in the files if you inspect them
|
||||
with a text editor.
|
||||
|
||||
The whole difference comes
|
||||
from the space in the panels.
|
||||
The width remains the same, but there is a gap
|
||||
in the middle in the new format.
|
||||
|
||||
At this moment, this is the simplest
|
||||
format we know on how to determine this.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
assert "Buildings" in self.layers and "Roofs" not in self.layers
|
||||
self.new_aurora_format = True
|
||||
except AssertionError:
|
||||
error = FileValidationMessage.OldDxfFormat.value
|
||||
raise OldDxfFormatException(error)
|
||||
|
||||
def is_new_aurora_format(self):
|
||||
""" return boolean """
|
||||
|
||||
return self.new_aurora_format
|
||||
171
helix/validators/file_validator.py
Normal file
171
helix/validators/file_validator.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from enum import Enum
|
||||
import pathlib
|
||||
|
||||
from helix.constants.file_validation_error import FileValidationError
|
||||
from helix.constants.file_validation_error import FileValidationMessage
|
||||
from helix.validators.csv_input_validator import CsvInputValidator
|
||||
from helix.validators.dxf_input_validator import DxfInputValidator
|
||||
|
||||
|
||||
class UnknownFileValidator(object):
|
||||
def __init__(self, _):
|
||||
pass
|
||||
|
||||
def validate(self, _):
|
||||
return FileValidationError(FileValidationMessage.UnknownFileUploaded,
|
||||
None)
|
||||
|
||||
|
||||
class FileType(Enum):
|
||||
Csv = 0
|
||||
AuroraDxf = 1
|
||||
Unknown = 2
|
||||
|
||||
@property
|
||||
def validator(self):
|
||||
"""Provide the proper implementation
|
||||
of the validation for the selected
|
||||
FileType
|
||||
|
||||
"""
|
||||
|
||||
return {
|
||||
FileType.Csv: CsvInputValidator,
|
||||
FileType.AuroraDxf: DxfInputValidator,
|
||||
FileType.Unknown: UnknownFileValidator
|
||||
}.get(self)
|
||||
|
||||
def valid_mapping(self, extension):
|
||||
"""Validates the extension obtained file
|
||||
|
||||
Arguments;
|
||||
extension: string
|
||||
Returns:
|
||||
boolean
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
mapping = {FileType.Csv: ["txt", "csv"],
|
||||
FileType.AuroraDxf: ["dxf"],
|
||||
FileType.Unknown: [None]}.get(self)
|
||||
assert extension in mapping
|
||||
except AssertionError:
|
||||
raise InvalidMappingException
|
||||
|
||||
def get_invalid_mapping_error(self):
|
||||
"""Get the validation message
|
||||
when choosing the wrong file for the
|
||||
selected FileType
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
|
||||
mapping = {FileType.Csv:
|
||||
FileValidationMessage.ExpectedTxtFile,
|
||||
FileType.AuroraDxf:
|
||||
FileValidationMessage.ExpectedDxfFile,
|
||||
FileType.Unknown:
|
||||
FileValidationMessage.UnknownFileUploaded}.get(self)
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
class FileValidator(object):
|
||||
"""Validates that the received stream
|
||||
belongs to a valid FileType
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
|
||||
def obtain_stream(self, path):
|
||||
"""Obtain the content of the file
|
||||
|
||||
Argument:
|
||||
path (str): A path that will be opened
|
||||
Returns:
|
||||
str The content of the file, otherwise an empty string
|
||||
|
||||
"""
|
||||
|
||||
content = ""
|
||||
try:
|
||||
content = path.read().decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
finally:
|
||||
return content
|
||||
|
||||
def validate(self, stream, file, expected):
|
||||
"""Validates the uploaded file by extension
|
||||
and content
|
||||
|
||||
Arguments;
|
||||
stream (string): File content
|
||||
file (FileObject): A file object provided from the ui
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
file_type = self.identify_file_type(stream)
|
||||
assert file_type == expected
|
||||
validator = file_type.validator(self.values)
|
||||
extension = self.obtain_extension(file)
|
||||
file_type.valid_mapping(extension)
|
||||
return validator.validate(stream)
|
||||
except AssertionError:
|
||||
return FileValidationError(expected.get_invalid_mapping_error(),
|
||||
None)
|
||||
except InvalidMappingException:
|
||||
error_type = file_type.get_invalid_mapping_error()
|
||||
return FileValidationError(error_type, None)
|
||||
except IndexError:
|
||||
error_type = FileValidationMessage.UnknownFileUploaded
|
||||
return FileValidationError(error_type, None)
|
||||
except InvalidExtensionException:
|
||||
error_type = FileValidationMessage.UnknownFileUploaded
|
||||
return FileValidationError(error_type, None)
|
||||
|
||||
def obtain_extension(self, file_object):
|
||||
"""Obtain the extension from the
|
||||
user uploaded file
|
||||
|
||||
Arguments:
|
||||
file_object (file): A file like object
|
||||
|
||||
"""
|
||||
|
||||
extension = pathlib.Path(file_object.filename).suffix
|
||||
file_pieces = extension.split(".")
|
||||
return file_pieces[1]
|
||||
|
||||
def identify_file_type(self, file):
|
||||
"""Determines the FileType object
|
||||
based on the beginning of the provided string
|
||||
|
||||
Arguments;
|
||||
file (str): The raw stream
|
||||
Returns:
|
||||
enum
|
||||
|
||||
"""
|
||||
|
||||
if file.startswith("999\r\nDesign created by Aurora"):
|
||||
return FileType.AuroraDxf
|
||||
|
||||
if file.startswith("HANDLE\t"):
|
||||
return FileType.Csv
|
||||
|
||||
return FileType.Unknown
|
||||
|
||||
|
||||
class InvalidExtensionException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMappingException(Exception):
|
||||
pass
|
||||
15
helix/validators/seismic_anchor_validator.py
Normal file
15
helix/validators/seismic_anchor_validator.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
from helix.constants.seismic_anchor_validation_error import SeismicAnchorValidationError
|
||||
|
||||
|
||||
class SeismicAnchorValidator(object):
|
||||
def __init__(self, calculator):
|
||||
self.calculator = calculator
|
||||
|
||||
def validate(self, panel_data):
|
||||
subarray_summary = self.calculator.subarray_summary()
|
||||
for subarray in subarray_summary:
|
||||
subarray_panels = extract_subarray(panel_data, subarray.subarray_number)
|
||||
seismic_anchor_count = sum(panel.seismic_anchors or 0 for panel in subarray_panels)
|
||||
if subarray.required_seismic_anchors > seismic_anchor_count:
|
||||
return SeismicAnchorValidationError.TooFewAnchors
|
||||
47
helix/validators/subarray_validator.py
Normal file
47
helix/validators/subarray_validator.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from helix.constants.subarray import SUBARRAY_SIZE_BIG
|
||||
from helix.constants.system_type import SystemType
|
||||
from helix.models.dxf.dxf_error import DXFError
|
||||
from helix.models.dxf.graph_direction import GraphDirection
|
||||
|
||||
|
||||
class SubarrayValidator(object):
|
||||
"""
|
||||
Tests to make sure that Single Tilt systems have East or West neighbors
|
||||
(as a panel without an East/West neighbor is
|
||||
invalid, and that Dual Tilt systems have North or South neighbors.
|
||||
Also checks to make sure that all "sides" of the
|
||||
subarray are less than 150' long (due to government safety requirements).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def validate_subarray(node_graph, subarray_number, system_type):
|
||||
subarray_node_graph = [node for node in node_graph if node.panel.subarray == subarray_number]
|
||||
furthest_west = subarray_node_graph[0].panel.coordinate.unrotate()
|
||||
furthest_east = subarray_node_graph[0].panel.coordinate.unrotate()
|
||||
furthest_north = subarray_node_graph[0].panel.coordinate.unrotate()
|
||||
furthest_south = subarray_node_graph[0].panel.coordinate.unrotate()
|
||||
|
||||
for node in subarray_node_graph:
|
||||
if system_type == SystemType.singleTilt:
|
||||
if not (node.has_existing_neighbor(GraphDirection.East) or node.has_existing_neighbor(GraphDirection.West)):
|
||||
raise DXFError("Error - Unsupported panel configuration in subarray %d." % subarray_number)
|
||||
if not (node.has_existing_neighbor(GraphDirection.North) or node.has_existing_neighbor(GraphDirection.South)):
|
||||
raise DXFError("Error - Unsupported panel configuration in subarray %d." % subarray_number)
|
||||
rotated_coordinate = node.panel.coordinate.unrotate()
|
||||
if rotated_coordinate.x < furthest_west.x:
|
||||
furthest_west = rotated_coordinate
|
||||
elif rotated_coordinate.x > furthest_east.x:
|
||||
furthest_east = rotated_coordinate
|
||||
if rotated_coordinate.y < furthest_south.y:
|
||||
furthest_south = rotated_coordinate
|
||||
elif rotated_coordinate.y > furthest_north.y:
|
||||
furthest_north = rotated_coordinate
|
||||
|
||||
# detect subarray size
|
||||
horizontal_distance = furthest_east.x - furthest_west.x
|
||||
if horizontal_distance > (150 * 12):
|
||||
raise DXFError(SUBARRAY_SIZE_BIG)
|
||||
|
||||
vertical_distance = furthest_north.y - furthest_south.y
|
||||
if vertical_distance > (150 * 12):
|
||||
raise DXFError(SUBARRAY_SIZE_BIG)
|
||||
Reference in New Issue
Block a user