Skip to content

Commit

Permalink
Settings, type-aware extension creation and "Namings" (#430)
Browse files Browse the repository at this point in the history
**Settings and Extensibles**

* Introduced Settings – type-sensitive values that can be configured externally in a configuration file or through other kind of metadata.
* Introduced SettingsDict to retrieve typed setting values
* Added typed Extension initialization in create_with_settings() and non-typed create_with_dict() which gets casted to SettingsDict
* Renamed all __options__ to extension_settings
* Renamed __desc__ to extension_desc
* Renamed __label__ to extension_label
* added `extension_name` to the class, not just registry
* added distill_settings()
* added Extensible.distill_settings()

**Naming:**

* Dropped Naming class, replaced with plain dictionary, moved most of the functionality into the Mapper (#431)
* Introduced NamingDict type alias
* Removed all references to naming, replaced either with mapper or a plain dictionary
* Naming is being read from a special section in the slicer.ini called `[naming]`. This will be later expanded to have multiple namings per configuration.
* Added naming documentation

**Browser and Workspace:**

* (browser) AggregationBrowser now asserts for cube instead of throwing normal exception
* (workspace) Workspace now requires Cube's store to be store name, not a store instance
* (ws) added explicit workspace settings and check for their validity in the config file
* (ws) create default naming in the workspace

**Slicer:**

* (slicer) Removed multiple cube aggregate, was dangerous to generate all cubes
  by missing and argument
* (slicer) Removed multiple cube denormalization, was dangerous (see above)

Recommended way: get list of cubes and then execute command once per
cube.

**Other:**

* changed map_base_attributes() to be a method of mapper
* query/cells: fixed dimension name typo
* fixed join types in the query generator
  • Loading branch information
Stiivi committed Apr 4, 2017
1 parent eeb15e8 commit 67ffc95
Show file tree
Hide file tree
Showing 19 changed files with 720 additions and 499 deletions.
51 changes: 29 additions & 22 deletions cubes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .errors import UserError, ConfigurationError, NoSuchDimensionError
from .common import read_json_file, sorted_dependencies
from .ext import Extensible
from .settings import Setting, SettingType

__all__ = (
"Authorizer",
Expand Down Expand Up @@ -192,28 +193,34 @@ def right_from_dict(info):
)

class SimpleAuthorizer(Authorizer, name="simple"):
__options__ = [
{
"name": "rights_file",
"description": "JSON file with access rights",
"type": "string"
},
{
"name": "roles_file",
"description": "JSON file with access right roles",
"type": "string"
},
{
"name": "order",
"description": "Order of allow/deny",
"type": "string",
"values": ["allow_deny", "deny_allow"]
},
{
"name": "guest",
"description": "Name of the 'guest' role",
"type": "string",
},
extension_settings = [
Setting(
name= "rights_file",
desc= "JSON file with access rights",
type= SettingType.str,
),
Setting(
name= "roles_file",
desc= "JSON file with access right roles",
type= SettingType.str,
),
Setting(
name= "order",
desc= "Order of allow/deny",
type= SettingType.str,
values= ["allow_deny", "deny_allow"]
),
Setting(
name= "guest",
desc= "Name of the 'guest' role",
type= SettingType.str,
),
Setting(
name= "identity_dimension",
desc= "Name of dimension which key is equivalent to the identity "
"token",
type= SettingType.str,
),

]

Expand Down
147 changes: 53 additions & 94 deletions cubes/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Collection,
Dict,
List,
Mapping,
NamedTuple,
Optional,
Type,
Expand All @@ -19,12 +20,15 @@

from .common import decamelize, coalesce_options
from .errors import ArgumentError, InternalError, ConfigurationError
# TODO: Reconsider need of SettingsDict
from .settings import Setting, SettingsDict, distill_settings, SettingValue

from importlib import import_module

__all__ = [
"EXTENSION_TYPES",
"ExtensionFinder",
"Extensible",
"ExtensionRegistry",
"get_registry",
]

# Known extension types.
Expand Down Expand Up @@ -81,83 +85,21 @@
"request_log_handler": "Request log handler",
}

from enum import Enum

from .errors import ArgumentError

TRUE_VALUES = ["1", "true", "yes"]
FALSE_VALUES = ["0", "false", "no"]


class Parameter:
name: str
default: Any
type: str
desc: Optional[str]
label: str
values: Collection[str]

def __init__(self,
name: str,
type_: Optional[str]=None,
default: Optional[Any]=None,
desc: Optional[str]=None,
label: Optional[str]=None,
values: Optional[Collection[str]]=None) -> None:
self.name = name
self.default = default
self.type = type_ or "string"
self.desc = desc
self.label = label or name
self.values = values or []

def coalesce_value(self, value: Any) -> Any:
""" Convert string into an object value of `value_type`. The type might
be: `string` (no conversion), `integer`, `float`
"""

return_value: Any

try:
if self.type == "string":
return_value = str(value)
elif self.type == "float":
return_value = float(value)
elif self.type == "integer":
return_value = int(value)
elif self.type == "bool":
if not value:
return_value = False
elif isinstance(value, str):
return_value = value.lower() in TRUE_VALUES
else:
return_value = bool(value)
else:
raise ConfigurationError(f"Unknown option value type {self.type}")

except ValueError:
label: str

if self.label:
label = f"parameter {self.label} "
else:
label = f"parameter {self.name} "

raise ConfigurationError(f"Unable to convert {label} value '{value}' "
f"into type {self.type}")

return return_value


T = TypeVar('T', bound=Type["Extensible"])

# Fixed:
# browser - (store)
# store - ()
# model_provider - (store)
# formatter: (result)
# authorizer: (store?)
# authenticator: ()
# request_log_handler: (store?)

class ExtensionDescription(NamedTuple):
type: str
name: str
label: str
doc: str
params: List[Parameter]
settings: List[Setting]


class ExtensionRegistry:
Expand Down Expand Up @@ -205,12 +147,15 @@ def names(self) -> Collection[str]:

def describe(self, name: str) -> ExtensionDescription:
ext = self.extension(name)
doc: str
doc = ext.extension_desc or ext.__doc__ or "(No documentation)"

desc = ExtensionDescription(
type= self.name,
name= name,
label= name,
doc= ext.__doc__ or "(no documentation)",
params = ext.__parameters__ or [])
label= ext.extension_label or name,
doc=doc,
settings = ext.extension_settings or [])

return desc

Expand Down Expand Up @@ -244,9 +189,14 @@ def get_registry(name: str) -> ExtensionRegistry:
return _registries[name]


T = TypeVar('T', bound="Extensible")

class Extensible:
__extension_type__ = "undefined"
__parameters__: List[Parameter] = []
extension_name: str = "undefined"
extension_settings: List[Setting] = []
extension_desc: Optional[str] = None
extension_label: Optional[str] = None

def __init_subclass__(cls, name: Optional[str]=None, abstract: bool=False) -> None:
assert cls.__extension_type__ in EXTENSION_TYPES, \
Expand All @@ -260,6 +210,7 @@ def __init_subclass__(cls, name: Optional[str]=None, abstract: bool=False) -> No
f"or abstract flag specified."

if name is not None:
cls.extension_name = name
registry: ExtensionRegistry
registry = get_registry(cls.__extension_type__)
registry.register_extension(name, cls)
Expand All @@ -272,28 +223,36 @@ def __init_subclass__(cls, name: Optional[str]=None, abstract: bool=False) -> No
# We do nothing for abstract subclasses
pass

# TODO: Once the design of extensions is done, review the following methods
# and remove those that are not being used.
@classmethod
def concrete_extension(cls: T, name: str) -> Type[T]:
def concrete_extension(cls: Type[T], name: str) -> Type[T]:
registry: ExtensionRegistry
registry = get_registry(cls.__extension_type__)
return cast(Type[T], registry.extension(name))

@classmethod
def create_with_params(cls: T, params: Dict[str, Any]) -> T:
kwargs: Dict[str, Any]
kwargs = {}

for param in cls.__parameters__:
if param.name in params:
value = params[param.name]
kwargs[param.name] = param.coalesce_value(value)
elif param.default:
value = param.default
kwargs[param.name] = param.coalesce_value(value)
else:
typename = cls.__extension_type__
raise ConfigurationError(f"Invalid parameter '{param.name}' "
f"for extension: {typename}")
def create_with_dict(cls: Type[T], mapping: Mapping[str, Any]) -> T:
settings: SettingsDict
settings = SettingsDict(mapping=mapping, settings=cls.extension_settings)

return cls.create_with_settings(settings)

@classmethod
def create_with_settings(cls: Type[T], settings: SettingsDict) -> T:
return cast(T, cls(**settings)) # type: ignore

@classmethod
def distill_settings(cls: Type[T], mapping: Mapping[str, Any]) \
-> Dict[str, Optional[SettingValue]]:
return distill_settings(mapping, cls.extension_settings)


return cast(T, cls(**kwargs)) # type: ignore
"""
- Extension has settings
- Extension has initialization arguments
- Initialization arguments might be provided by converting a setting value to
an object within owner's context
"""
23 changes: 12 additions & 11 deletions cubes/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from .errors import ArgumentError
from . import ext
from .settings import Setting, SettingType

from .query.constants import SPLIT_DIMENSION_NAME

Expand Down Expand Up @@ -270,12 +271,12 @@ def coalesce_table_labels(attributes, onrows, oncolumns):


class CrossTableFormatter(Formatter, name="cross_table"):
__options__ = [
{
"name": "indent",
"type": "integer",
"label": "Output indent"
},
extension_settings = [
Setting(
name= "indent",
type= SettingType.int,
label= "Output indent",
),
]

mime_type = "application/json"
Expand Down Expand Up @@ -320,11 +321,11 @@ def format(self, cube, result, onrows=None, oncolumns=None, aggregates=None,


class HTMLCrossTableFormatter(CrossTableFormatter, name="html_cross_table"):
__options__ = [
{
"name": "table_style",
"description": "CSS style for the table"
}
extension_settings = [
Setting(
name= "table_style",
desc= "CSS style for the table"
)
]
mime_type = "text/html"

Expand Down
13 changes: 8 additions & 5 deletions cubes/query/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
calculators_for_aggregates,
)

from ..settings import SettingsDict

from .constants import SPLIT_DIMENSION_NAME, NULL_PATH_VALUE

from .result import AggregationResult, Facts
Expand Down Expand Up @@ -149,15 +151,16 @@ class AggregationBrowser(Extensible, abstract=True):

def __init__(self,
cube: Cube,
store: Store=None,
locale: str=None,
**options: Any) -> None:
store: Optional[Store]=None,
locale: Optional[str]=None,
calendar: Optional[Calendar]=None,
) -> None:
"""Creates and initializes the aggregation browser. Subclasses should
override this method. """
super(AggregationBrowser, self).__init__()

if not cube:
raise ArgumentError("No cube given for aggregation browser")
assert cube is not None, \
"No cube given for aggregation browser"

self.cube = cube
self.store = store
Expand Down
2 changes: 1 addition & 1 deletion cubes/query/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ def cut_from_string(string: str,

converter = member_converters.get(dim_name)
if cube:
role = cube.dimension(dimension).role
role = cube.dimension(dim_name).role
if role is not None:
converter = converter or role_member_converters.get(role)
dimension = cube.dimension(dim_name)
Expand Down

0 comments on commit 67ffc95

Please sign in to comment.