Skip to content

Commit 7ffb068

Browse files
authored
Merge: PR #13 Prerelease optimizations (performance and introspection).
Final prerelease optimizations
2 parents 4cad69f + 8404575 commit 7ffb068

File tree

4 files changed

+57
-32
lines changed

4 files changed

+57
-32
lines changed

src/cadenzaanalytics/cadenza_analytics_extension_service.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""Provides a service which encapsulates the configuration and execution of individual analytics extensions.
22
Runs an HTTP server which executes the individual extension's analytics function and serves an extension
33
discovery endpoint."""
4+
import inspect
45
import json
5-
import errno
6-
import sys
76
from typing import Optional
87

98
from flask import Flask, Response
@@ -44,24 +43,24 @@ def add_analytics_extension(self, analytics_extension: CadenzaAnalyticsExtension
4443
----------
4544
analytics_extension : CadenzaAnalyticsExtension
4645
The analytics extension to be added.
47-
"""
4846
47+
Raises
48+
------
49+
ValueError
50+
If the relative path is already in use or the analytics function has an invalid signature.
51+
"""
4952
self.logger.info('Registering extension "%s" on relative path "%s"...',
5053
analytics_extension.print_name,
5154
analytics_extension.relative_path
5255
)
5356

5457
# perform some validation checks
5558
if analytics_extension.relative_path in [x.relative_path for x in self._analytics_extensions]:
56-
self.logger.critical('Relative path "%s" is already in use by another extension. Exiting...',
57-
analytics_extension.relative_path)
58-
sys.exit(errno.EINTR)
59-
if analytics_extension._analytics_function.__code__.co_argcount != 1: # pylint: disable=W0212
60-
self.logger.critical('The analytics function "%s()" takes 1 positional arguments, but %s given. Exiting...',
61-
analytics_extension._analytics_function.__name__, # pylint: disable=W0212
62-
analytics_extension._analytics_function.__code__.co_argcount # pylint: disable=W0212
63-
)
64-
sys.exit(errno.EINTR)
59+
msg = f'Relative path "{analytics_extension.relative_path}" is already in use by another extension.'
60+
self.logger.critical(msg)
61+
raise ValueError(msg)
62+
63+
self._validate_analytics_function(analytics_extension._analytics_function) # pylint: disable=W0212
6564

6665
self._analytics_extensions.append(analytics_extension)
6766

@@ -98,6 +97,30 @@ def app(self) -> Flask:
9897
"""
9998
return self._app
10099

100+
def _validate_analytics_function(self, func) -> None:
101+
"""Validate that analytics function has correct signature.
102+
103+
Parameters
104+
----------
105+
func : Callable
106+
The analytics function to validate.
107+
108+
Raises
109+
------
110+
ValueError
111+
If the function does not accept exactly 1 positional argument.
112+
"""
113+
try:
114+
sig = inspect.signature(func)
115+
params = [p for p in sig.parameters.values()
116+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY,
117+
inspect.Parameter.POSITIONAL_OR_KEYWORD)]
118+
if len(params) != 1:
119+
raise ValueError(f"Function must accept exactly 1 positional argument, got {len(params)}")
120+
except (ValueError, TypeError) as err:
121+
self.logger.critical("Invalid analytics function: %s", err)
122+
raise ValueError(str(err)) from err
123+
101124
def _list_extensions(self) -> Response:
102125
"""List all registered analytics extensions.
103126

src/cadenzaanalytics/data/attribute_group.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
class AttributeGroup(DataObject):
99
"""Defines a group of attributes with common characteristics for input data.
1010
11-
Attribute groups specify accepted (non-geometry) data types, cardinality
12-
constraints, and optionally geometry types and spatial reference system for
11+
Attribute groups specify accepted (non-geometry) data types, cardinality
12+
constraints, and optionally geometry types and spatial reference system for
1313
data columns.
1414
"""
1515

src/cadenzaanalytics/data/parameter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(self, *,
4949
List of allowed values for SELECT parameters.
5050
required : bool, optional
5151
Whether the parameter is required, by default False.
52-
For parameter type boolean, required=True makes submitting the value
52+
For parameter type boolean, required=True makes submitting the value
5353
True mandatory.
5454
default_value : Any, optional
5555
Default value if the user doesn't provide one.

src/cadenzaanalytics/request/request_metadata.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ def __init__(self, request_metadata: dict) -> None:
2323
self._columns = [ColumnMetadata._from_dict(column)
2424
for column
2525
in RequestMetadata._parse_columns(request_metadata)]
26+
# Build index for O(1) lookups by column name
27+
self._columns_by_name: Dict[str, ColumnMetadata] = {col.name: col for col in self._columns}
28+
# Build groups index once at initialization
29+
self._groups: Dict[str, List[ColumnMetadata]] = {}
30+
for column in self._columns:
31+
self._groups.setdefault(column.attribute_group_name, []).append(column)
2632

2733
@staticmethod
2834
def _parse_columns(request_metadata: dict) -> List[dict]:
@@ -42,26 +48,26 @@ def __getitem__(self, key: str) -> ColumnMetadata:
4248
Returns
4349
-------
4450
ColumnMetadata
45-
Metadata for the column
46-
"""
47-
48-
for column in self._columns:
49-
if column.name == key:
50-
return column
51+
Metadata for the column.
5152
52-
raise KeyError(f"Column '{key}' not found.")
53+
Raises
54+
------
55+
KeyError
56+
If no column with the given name exists.
57+
"""
58+
try:
59+
return self._columns_by_name[key]
60+
except KeyError:
61+
raise KeyError(f"Column '{key}' not found.") from None
5362

5463
def __iter__(self) -> Iterator[str]:
5564
return iter(c.name for c in self._columns)
5665

5766
def __len__(self) -> int:
5867
return len(self._columns)
5968

60-
def __contains__(self, key: str) -> bool:
61-
for column in self._columns:
62-
if column.name == key:
63-
return True
64-
return False
69+
def __contains__(self, key: object) -> bool:
70+
return key in self._columns_by_name
6571

6672
@property
6773
def ids(self) -> Optional[List[ColumnMetadata]]:
@@ -96,11 +102,7 @@ def groups(self) -> Dict[str, List[ColumnMetadata]]:
96102
A dictionary where the keys are the attribute group names and values are lists of corresponding
97103
column metadata objects.
98104
"""
99-
grouped_columns = {}
100-
for column in self._columns:
101-
grouped_columns.setdefault(column.attribute_group_name, []).append(column)
102-
103-
return grouped_columns
105+
return self._groups
104106

105107
@property
106108
def columns(self) -> List[ColumnMetadata]:

0 commit comments

Comments
 (0)