ooui 0.1.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.
ooui-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GISCE-TI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ooui-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: ooui
3
+ Version: 0.1.0
4
+ Summary: Open Object User Interface
5
+ Home-page: https://github.com/gisce/python-ooui
6
+ Author: GISCE
7
+ Author-email: devel@gisce.net
8
+ License: MIT
9
+ Provides: ooui
10
+ License-File: LICENSE
11
+
12
+ Open Object User Interface for GISCE-ERP
ooui-0.1.0/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # python-ooui
2
+ Port of ooui.js to Python
File without changes
@@ -0,0 +1,30 @@
1
+ from __future__ import absolute_import, unicode_literals
2
+ from lxml import etree
3
+ from ooui.graph.indicator import GraphIndicator
4
+ from ooui.graph.chart import GraphChart
5
+
6
+
7
+ GRAPH_TYPES = {
8
+ 'indicator': GraphIndicator,
9
+ 'line': GraphChart,
10
+ 'pie': GraphChart,
11
+ 'bar': GraphChart,
12
+ }
13
+
14
+
15
+ def parse_graph(xml):
16
+ """
17
+ Parse a graph from an XML string.
18
+ :param xml:
19
+ :return:
20
+ :rtype ooui.graph.Graph
21
+ """
22
+ tree = etree.fromstring(xml)
23
+ graph = tree.xpath('//graph')[0]
24
+
25
+ graph_type = graph.get("type")
26
+
27
+ if not graph_type or graph_type not in GRAPH_TYPES:
28
+ raise ValueError("{} is not a valid graph".format(graph_type))
29
+
30
+ return GRAPH_TYPES[graph_type](graph_type, graph)
@@ -0,0 +1,100 @@
1
+ class GraphAxis(object):
2
+ AXIS_OPTIONS = ('x', 'y')
3
+
4
+ def __init__(self, name, axis):
5
+ if axis not in self.AXIS_OPTIONS:
6
+ raise ValueError("Invalid axis value. Must be 'x' or 'y'.")
7
+
8
+ self._name = name
9
+ self._axis = axis
10
+
11
+ @property
12
+ def name(self):
13
+ return self._name
14
+
15
+ @property
16
+ def axis(self):
17
+ return self._axis
18
+
19
+
20
+ class GraphXAxis(GraphAxis):
21
+ def __init__(self, name):
22
+ # Crida el constructor de GraphAxis amb l'eix 'x' fixat
23
+ super(GraphXAxis, self).__init__(name, 'x')
24
+
25
+
26
+ class GraphYAxis(GraphAxis):
27
+ OPERATOR_OPTIONS = ('count', '+', '-', '*', 'min', 'max', 'avg')
28
+
29
+ def __init__(self, name, operator, label=None, stacked=None):
30
+ super(GraphYAxis, self).__init__(name, 'y')
31
+
32
+ if operator not in self.OPERATOR_OPTIONS:
33
+ raise ValueError("Invalid operator value.")
34
+
35
+ self._operator = operator
36
+ self._label = label
37
+ self._stacked = stacked
38
+
39
+ @property
40
+ def operator(self):
41
+ return self._operator
42
+
43
+ @property
44
+ def label(self):
45
+ return self._label
46
+
47
+ @property
48
+ def stacked(self):
49
+ return self._stacked
50
+
51
+
52
+ def parse_xy_axis(nodes):
53
+ x_axis = None
54
+ y_axes = []
55
+
56
+ for child in nodes:
57
+ if child.tag == "field":
58
+ axis = child.get("axis")
59
+ name = child.get("name")
60
+ operator = child.get("operator")
61
+ label = child.get("label")
62
+ stacked = child.get("stacked")
63
+
64
+ if not axis:
65
+ raise ValueError("Field {} doesn't have an axis".format(name))
66
+
67
+ if not name:
68
+ raise ValueError("Missing name attribute for field")
69
+
70
+ if axis == "x":
71
+ x_axis = GraphXAxis(name)
72
+ elif axis == "y":
73
+ y_axes.append(GraphYAxis(name, operator, label, stacked))
74
+
75
+ if not x_axis:
76
+ raise ValueError("No x axis found")
77
+
78
+ if not y_axes:
79
+ raise ValueError("No y axis found. At least one y axis is required")
80
+
81
+ return {"x": x_axis, "y": y_axes}
82
+
83
+
84
+ def get_y_axis_fieldname(y_axis, fields):
85
+ """
86
+ Retrieve the field name for a Y-axis based on the `GraphYAxis` object
87
+ and field definitions.
88
+
89
+ :param GraphYAxis y_axis: An object representing the Y-axis.
90
+ :param dict fields: A dictionary containing the field definitions.
91
+
92
+ :rtype: str
93
+ :returns: The name of the Y-axis field.
94
+ """
95
+ field_props = fields.get(y_axis.name, {})
96
+
97
+ if 'string' in field_props and field_props['string']:
98
+ return field_props['string']
99
+
100
+ return y_axis.name
@@ -0,0 +1,31 @@
1
+
2
+
3
+ class Graph(object):
4
+ def __init__(self, element):
5
+ """
6
+
7
+ :param element: lxml.etree._Element
8
+ """
9
+ self._string = element.get('string')
10
+ self._timerange = element.get('timerange', None)
11
+
12
+ interval = element.get('interval', None)
13
+ self._interval = int(interval) if interval is not None else 1
14
+
15
+ self._type = None
16
+
17
+ @property
18
+ def string(self):
19
+ return self._string
20
+
21
+ @property
22
+ def timerange(self):
23
+ return self._timerange
24
+
25
+ @property
26
+ def interval(self):
27
+ return self._interval
28
+
29
+ @property
30
+ def type(self):
31
+ return self._type
@@ -0,0 +1,20 @@
1
+ from ooui.graph.base import Graph
2
+ from ooui.graph.axis import parse_xy_axis
3
+
4
+
5
+ class GraphChart(Graph):
6
+ def __init__(self, graph_type, element):
7
+ super(GraphChart, self).__init__(element)
8
+
9
+ self._type = graph_type
10
+ xy_axis = parse_xy_axis(element)
11
+ self._x = xy_axis['x']
12
+ self._y = xy_axis['y']
13
+
14
+ @property
15
+ def x(self):
16
+ return self._x
17
+
18
+ @property
19
+ def y(self):
20
+ return self._y
@@ -0,0 +1,116 @@
1
+ from functools import reduce
2
+
3
+
4
+ def get_fields_to_retrieve(ooui):
5
+ """
6
+ Returns a list of fields to retrieve from the ooui
7
+ :param ooui: Graph instance
8
+ :type ooui: ooui.graph.chart.GraphChart
9
+ :return: list of fields to retrieve
10
+ :rtype: list[str]
11
+ """
12
+ x_field = ooui.x.name
13
+ fields = [x_field]
14
+
15
+ if not ooui.y:
16
+ return []
17
+
18
+ for y in ooui.y:
19
+ if y.operator != 'count' and y.name not in fields:
20
+ fields.append(y.name)
21
+
22
+ if y.label and y.label not in fields:
23
+ fields.append(y.label)
24
+
25
+ return fields
26
+
27
+
28
+ def get_value_and_label_for_field(fields, values, field_name):
29
+ """
30
+ Retrieve the value and label for a specific field.
31
+
32
+ :param dict fields: A dictionary containing the field definitions.
33
+ The keys are field names and the values are their details.
34
+ :param dict values: A dictionary containing the field values.
35
+ The keys are field names, and the values are the associated values.
36
+ :param str field_name: The name of the field for which to obtain the value
37
+ and label.
38
+
39
+ :rtype: dict
40
+ :returns: A dictionary with the keys `value` and `label`:
41
+ - `value`: The field value or `False` if not found.
42
+ - `label`: The field label or `None` if not found.
43
+
44
+ :raises ValueError: If the specified field is not found in the `fields`
45
+ dictionary.
46
+ """
47
+ x_field_data = fields.get(field_name)
48
+ value = values.get(field_name)
49
+
50
+ if not x_field_data:
51
+ raise ValueError("Field {} not found".format(field_name))
52
+
53
+ if x_field_data['type'] == 'many2one':
54
+ if not value:
55
+ return {'value': False, 'label': None}
56
+ return {'value': value[0], 'label': value[1]}
57
+
58
+ elif x_field_data['type'] == 'selection':
59
+ selection_values = x_field_data['selection']
60
+ value_pair = next((pair for pair in selection_values if pair[0] == value), None)
61
+
62
+ if not value_pair:
63
+ return {'value': False, 'label': None}
64
+
65
+ return {'value': value, 'label': value_pair[1]}
66
+
67
+ return {'value': value, 'label': value}
68
+
69
+
70
+ def get_value_for_operator(operator, values):
71
+ """
72
+ Retrieve the result of applying an operator on a list of values.
73
+
74
+ :param str operator: The operator to be applied.
75
+ Possible values include "count", "+", "-", "*", "avg", "min", "max".
76
+ :param list values: A list of numerical values on which to apply the
77
+ operator.
78
+
79
+ :rtype: float or int
80
+ :returns: The result of applying the operator to the values.
81
+
82
+ :raises ValueError: If an unsupported operator is provided or if the
83
+ values list is empty for certain operators.
84
+ """
85
+ if operator == "count":
86
+ return len(values)
87
+ elif operator == "+":
88
+ return round_number(sum(values))
89
+ elif operator == "-":
90
+ return round_number(reduce(lambda x, y: x - y, values))
91
+ elif operator == "*":
92
+ return round_number(reduce(lambda x, y: x * y, values))
93
+ elif operator == "avg":
94
+ if not values:
95
+ return 0
96
+ total_sum = sum(values)
97
+ avg = total_sum / len(values)
98
+ return round_number(avg)
99
+ elif operator == "min":
100
+ return min(values)
101
+ elif operator == "max":
102
+ return max(values)
103
+ else:
104
+ raise ValueError("Unsupported operator: {}".format(operator))
105
+
106
+
107
+ def round_number(num):
108
+ """
109
+ Round a number to two decimal places.
110
+
111
+ :param float or int num: The number to be rounded.
112
+
113
+ :rtype: float
114
+ :returns: The number rounded to two decimal places.
115
+ """
116
+ return round(num * 100) / 100
@@ -0,0 +1,39 @@
1
+ from ooui.graph.base import Graph
2
+ from ooui.helpers import parse_bool_attribute, replace_entities
3
+
4
+
5
+ class GraphIndicator(Graph):
6
+ def __init__(self, graph_type, element):
7
+ # Inicia la classe base Graph
8
+ super(GraphIndicator, self).__init__(element)
9
+
10
+ self._type = graph_type
11
+ self._color = replace_entities(element.get('color')) if element.get(
12
+ 'color') else None
13
+ self._icon = replace_entities(element.get('icon')) if element.get(
14
+ 'icon') else None
15
+ self._suffix = element.get('suffix') if element.get('suffix') else None
16
+ self._total_domain = replace_entities(
17
+ element.get('totalDomain')) if element.get('totalDomain') else None
18
+ self._show_percent = parse_bool_attribute(
19
+ element.get('showPercent')) if element.get('showPercent') else False
20
+
21
+ @property
22
+ def color(self):
23
+ return self._color
24
+
25
+ @property
26
+ def icon(self):
27
+ return self._icon
28
+
29
+ @property
30
+ def total_domain(self):
31
+ return self._total_domain
32
+
33
+ @property
34
+ def show_percent(self):
35
+ return self._show_percent
36
+
37
+ @property
38
+ def suffix(self):
39
+ return self._suffix
@@ -0,0 +1,186 @@
1
+ from __future__ import absolute_import, unicode_literals
2
+ from ooui.graph.fields import get_value_and_label_for_field, get_value_for_operator
3
+ from ooui.graph.axis import get_y_axis_fieldname
4
+ from ooui.graph.timerange import process_timerange_data
5
+
6
+
7
+ def process_graph_data(ooui, values, fields, options=None):
8
+ """
9
+ Process graph data by grouping and sorting the values according to the
10
+ specified X and Y axes.
11
+
12
+ :param ooui: A GraphChart-like object containing chart information.
13
+ :type ooui: ooui.graph.GraphChart
14
+ :param list values: A list of dictionaries representing the original data.
15
+ :param dict fields: A dictionary of field definitions.
16
+ :param dict options: Optional additional options for processing graph data.
17
+
18
+ :rtype: dict
19
+ :returns: A dictionary containing the final processed data and flags like
20
+ isGroup and isStack.
21
+ """
22
+ if options is None:
23
+ options = {}
24
+
25
+ values_grouped_by_x = get_values_grouped_by_field(
26
+ ooui.x.name, fields, values
27
+ )
28
+
29
+ data = []
30
+
31
+ # Iterate through the y-axis items
32
+ for y_field in ooui.y:
33
+ for x_value, group in values_grouped_by_x.items():
34
+ x_label = group['label']
35
+ objects_for_x_value = group['entries']
36
+
37
+ if not y_field.label:
38
+ values_for_y_field = get_values_for_y_field(
39
+ objects_for_x_value, y_field.name, fields
40
+ )
41
+ final_value = get_value_for_operator(
42
+ y_field.operator, values_for_y_field
43
+ )
44
+
45
+ data.append({
46
+ 'x': x_label or False,
47
+ 'value': final_value,
48
+ 'type': get_y_axis_fieldname(y_field, fields),
49
+ 'operator': y_field.operator,
50
+ 'stacked': y_field.stacked
51
+ })
52
+ else:
53
+ values_grouped_by_y_label = get_values_grouped_by_field(
54
+ y_field.label, fields, objects_for_x_value
55
+ )
56
+
57
+ for y_unique_value, grouped_entries in values_grouped_by_y_label.items():
58
+ entries = grouped_entries['entries']
59
+ label = grouped_entries['label']
60
+
61
+ values_for_y_field = get_values_for_y_field(
62
+ entries, y_field.name, fields
63
+ )
64
+ final_value = get_value_for_operator(
65
+ y_field.operator, values_for_y_field
66
+ )
67
+
68
+ data.append({
69
+ 'x': x_label or False,
70
+ 'value': final_value,
71
+ 'type': label,
72
+ 'operator': y_field.operator,
73
+ 'stacked': y_field.stacked
74
+ })
75
+
76
+ # Check if data should be flagged as grouped or stacked
77
+ is_group = any(y.label is not None for y in ooui.y)
78
+ is_stack = any(y.stacked is not None for y in ooui.y)
79
+
80
+ # Sort the data by the x-axis
81
+ sorted_data = sorted(data, key=lambda x: x['x'] or "")
82
+
83
+ adjusted_stacked_data = sorted_data[:]
84
+ if is_stack and len([y for y in ooui.y if y.stacked is not None]) > 1:
85
+ adjusted_stacked_data = [
86
+ dict(
87
+ entry,
88
+ type="{} - {}".format(entry['type'], entry['stacked'])
89
+ ) for entry in sorted_data
90
+ ]
91
+
92
+ adjusted_uninformed_data = adjusted_stacked_data[:]
93
+ if ooui.type == 'pie' and any(entry['x'] is False for entry in adjusted_stacked_data):
94
+ adjusted_uninformed_data = [
95
+ dict(
96
+ entry,
97
+ x=options.get('uninformedString', 'Not informed') if entry['x'] is False else entry['x']
98
+ ) for entry in adjusted_stacked_data
99
+ ]
100
+ else:
101
+ adjusted_uninformed_data = [
102
+ entry for entry in adjusted_stacked_data if entry['x'] is not False
103
+ ]
104
+
105
+ # Fill gaps if a timerange is defined
106
+ final_data = adjusted_uninformed_data
107
+ if ooui.timerange:
108
+ final_data = process_timerange_data(
109
+ final_data, ooui.timerange, ooui.interval
110
+ )
111
+ elif ooui.type == 'pie':
112
+ final_data = sorted(
113
+ adjusted_uninformed_data, key=lambda x: x['value'], reverse=True
114
+ )
115
+
116
+ return {
117
+ 'data': final_data,
118
+ 'isGroup': is_stack or is_group,
119
+ 'isStack': is_stack
120
+ }
121
+
122
+
123
+ def get_values_for_y_field(entries, field_name, fields):
124
+ """
125
+ Retrieve labels for a specified field across multiple entries.
126
+
127
+ :param list entries: A list of dictionaries representing the entries.
128
+ :param str field_name: The name of the field for which to retrieve labels.
129
+ :param dict fields: A dictionary containing the field definitions.
130
+ :param func get_value_and_label_for_field: Function to retrieve value and
131
+ label for a field.
132
+
133
+ :rtype: list
134
+ :returns: A list containing the labels corresponding to the specified field.
135
+ """
136
+ return [
137
+ get_value_and_label_for_field(fields, entry, field_name)['label']
138
+ for entry in entries
139
+ ]
140
+
141
+
142
+ def get_values_grouped_by_field(field_name, fields, values):
143
+ """
144
+ Group values by a specific field.
145
+
146
+ :param str field_name: The name of the field by which to group values.
147
+ :param dict fields: A dictionary containing field definitions.
148
+ :param list values: A list of dictionaries representing the values to be
149
+ grouped.
150
+
151
+ :rtype: dict
152
+ :returns: A dictionary where keys are field values and values are
153
+ dictionaries containing a label and an "entries" list.
154
+ """
155
+ grouped_values = {}
156
+
157
+ for entry in values:
158
+ result = get_value_and_label_for_field(fields, entry, field_name)
159
+ value, label = result['value'], result['label']
160
+
161
+ if value not in grouped_values:
162
+ grouped_values[value] = {'label': label, 'entries': []}
163
+
164
+ grouped_values[value]['entries'].append(entry)
165
+
166
+ return grouped_values
167
+
168
+
169
+ def get_all_objects_in_grouped_values(grouped):
170
+ """
171
+ Retrieve all objects within grouped values.
172
+
173
+ :param dict grouped: A dictionary where keys are group identifiers and
174
+ values are dictionaries with a key "entries" containing lists of
175
+ objects.
176
+
177
+ :rtype: list
178
+ :returns: A list containing all objects from each group's "entries".
179
+ """
180
+ total_objects = []
181
+
182
+ for key in grouped:
183
+ group = grouped[key]
184
+ total_objects.extend(group['entries'])
185
+
186
+ return total_objects
@@ -0,0 +1,300 @@
1
+ from datetime import datetime, timedelta
2
+ from dateutil.relativedelta import relativedelta
3
+
4
+ from ooui.graph.fields import get_value_for_operator
5
+
6
+
7
+ def process_timerange_data(values, timerange, interval=1):
8
+ """
9
+ Process time range data by combining values and filling gaps.
10
+
11
+ :param list values: A list de diccionaris representing the original data.
12
+ :param str timerange: The time range unit ("day", "week", "month", "year").
13
+ :param int interval: The interval to increment dates by.
14
+
15
+ :rtype: list
16
+ :returns: A list containing the processed values with gaps filled.
17
+ """
18
+ combined_values = combine_values_for_timerange(values, timerange)
19
+ filled_values = fill_gaps_in_timerange_data(
20
+ combined_values, timerange, interval
21
+ )
22
+
23
+ return filled_values
24
+
25
+
26
+ def fill_gaps_in_timerange_data(values, timerange, interval):
27
+ """
28
+ Fill gaps in time range data by inserting missing dates.
29
+
30
+ :param list values: A list of dictionaries representing the original data.
31
+ :param str timerange: The time range unit ("day", "week", "month", "year").
32
+ :param int interval: The interval to increment dates by.
33
+
34
+ :rtype: list
35
+ :returns: A new list containing all values with gaps filled in.
36
+ """
37
+ final_values = []
38
+ unique_values = get_unique_values_grouped_by(values, 'type-stacked')
39
+
40
+ for key, values_for_key in unique_values.items():
41
+ for i in range(len(values_for_key)):
42
+ value = values_for_key[i]
43
+ final_values.append(value)
44
+
45
+ if i == len(values_for_key) - 1:
46
+ break
47
+
48
+ date = value['x']
49
+ next_date = values_for_key[i + 1]['x']
50
+
51
+ missing_dates = get_missing_consecutive_dates(
52
+ [date, next_date], timerange, interval
53
+ )
54
+
55
+ final_values.extend([
56
+ {
57
+ 'x': string_date,
58
+ 'value': 0,
59
+ 'type': value['type'],
60
+ 'stacked': value['stacked']
61
+ } for string_date in missing_dates
62
+ ])
63
+
64
+ return final_values
65
+
66
+
67
+ def add_time_unit(start_date, interval, units):
68
+ if units == 'days':
69
+ return start_date + timedelta(days=interval)
70
+ elif units == 'weeks':
71
+ return start_date + timedelta(weeks=interval)
72
+ elif units == 'months':
73
+ return start_date + relativedelta(months=interval)
74
+ elif units == 'years':
75
+ return start_date + relativedelta(years=interval)
76
+ else:
77
+ raise ValueError("Unsupported units: {}".format(units))
78
+
79
+
80
+ def get_missing_consecutive_dates(dates, timerange, interval=1):
81
+ """
82
+ Retrieve the missing consecutive dates within a range according to a specific time unit.
83
+
84
+ :param list dates: A list of strings representing the sorted original dates.
85
+ :param str timerange: The time unit for checking consecutive dates ("day", "week", "month", "year").
86
+ :param int interval: An optional interval to increment dates by.
87
+
88
+ :rtype: list
89
+ :returns: A list of missing dates as strings.
90
+ """
91
+ missing_dates = []
92
+ units = "{}s".format(timerange)
93
+ format_str = get_format_for_units(units)
94
+
95
+ if len(dates) == 1:
96
+ return dates
97
+
98
+ sorted_dates = sorted(dates)
99
+
100
+ for i in range(len(sorted_dates) - 1):
101
+ date1 = datetime.strptime(sorted_dates[i], format_str)
102
+ date2 = datetime.strptime(sorted_dates[i + 1], format_str)
103
+
104
+ next_date = add_time_unit(date1, interval, units)
105
+
106
+ while next_date < date2:
107
+ missing_dates.append(next_date.strftime(format_str))
108
+ next_date = add_time_unit(next_date, interval, units)
109
+
110
+ return missing_dates
111
+
112
+
113
+ def combine_values_for_timerange(values, timerange):
114
+ """
115
+ Combine values according to a specific time range.
116
+
117
+ :param list values: A list of dictionaries representing the original data.
118
+ :param str timerange: The time range for adjusting and combining the values.
119
+
120
+ :rtype: list
121
+ :returns: A list of dictionaries containing the final combined values.
122
+ """
123
+ adjusted_values = adjust_x_values_for_time_range(values, timerange)
124
+ unique_values = get_unique_values_grouped_by(adjusted_values, 'all')
125
+
126
+ final_values = []
127
+
128
+ for key, values_for_key in unique_values.items():
129
+ operator = values_for_key[0]['operator']
130
+ if operator == 'count':
131
+ operator = '+'
132
+
133
+ final_value = get_value_for_operator(
134
+ operator, [entry['value'] for entry in values_for_key]
135
+ )
136
+
137
+ final_values.append(dict(values_for_key[0], value=final_value))
138
+
139
+ return final_values
140
+
141
+
142
+ def adjust_x_values_for_time_range(values, timerange):
143
+ """
144
+ Adjust the "x" values in a list of items according to the specified
145
+ time range.
146
+
147
+ :param list values: A list of dictionaries containing the original data.
148
+ :param str timerange: The time range for adjusting the "x" values.
149
+
150
+ :rtype: list
151
+ :returns: A new list where each item's "x" property is adjusted based on
152
+ the time range.
153
+ """
154
+ return [
155
+ dict(value, x=convert_date_to_time_range_adjusted(value['x'], timerange))
156
+ for value in values
157
+ ]
158
+
159
+
160
+ def convert_date_to_time_range_adjusted(date, timerange):
161
+ """
162
+ Adjust a date to a specific time range format.
163
+
164
+ :param str date: The original date string.
165
+ :param str timerange: The time range for adjustment
166
+ ("hour", "day", "week", "month", "year").
167
+
168
+ :rtype: str
169
+ :returns: The adjusted date in the specified time range format.
170
+ """
171
+ format_str = get_date_format(date)
172
+ moment_date = datetime.strptime(date, format_str)
173
+
174
+ if timerange == 'hour':
175
+ return moment_date.strftime('%Y-%m-%d %H:00')
176
+ elif timerange == 'day':
177
+ return moment_date.strftime('%Y-%m-%d')
178
+ elif timerange == 'week':
179
+ return moment_date.strftime('%Y-%W')
180
+ elif timerange == 'month':
181
+ return moment_date.strftime('%Y-%m')
182
+ elif timerange == 'year':
183
+ return moment_date.strftime('%Y')
184
+ else:
185
+ raise ValueError("Unsupported timerange: {}".format(timerange))
186
+
187
+
188
+ def get_date_format(date_str):
189
+ """
190
+ Determine the appropriate date format string based on whether the input
191
+ string contains a colon.
192
+
193
+ :param str date_str: A string representing the date to be checked.
194
+
195
+ :rtype: str
196
+ :returns: The appropriate date format string compatible with
197
+ `datetime.strptime`.
198
+ """
199
+ if ':' in date_str:
200
+ return '%Y-%m-%d %H:%M:%S'
201
+ else:
202
+ return '%Y-%m-%d'
203
+
204
+
205
+ def get_unique_values_grouped_by(values, group_by):
206
+ """
207
+ Group values by unique keys determined by the grouping criteria.
208
+
209
+ :param list values: A list of dictionaries representing the values.
210
+ :param str group_by: Grouping criteria. Can be "all" or "type-stacked".
211
+
212
+ :rtype: dict
213
+ :returns: A dictionary where keys are unique identifiers based on the
214
+ grouping criteria, and values are lists of grouped items.
215
+ """
216
+ unique_values = {}
217
+
218
+ for value in values:
219
+ x = value.get('x')
220
+ value_type = value.get('type')
221
+ stacked = value.get('stacked')
222
+
223
+ if group_by == 'all':
224
+ unique_key = "{}-{}-{}".format(x, value_type, stacked)
225
+ else:
226
+ unique_key = "{}-{}".format(value_type, stacked)
227
+
228
+ if unique_key not in unique_values:
229
+ unique_values[unique_key] = []
230
+
231
+ unique_values[unique_key].append(value)
232
+
233
+ return unique_values
234
+
235
+
236
+ def get_format_for_units(units):
237
+ """
238
+ Retrieve the appropriate date format string based on the units provided.
239
+
240
+ :param str units: A string representing the time units
241
+ (e.g., "days", "weeks", "months", "years", or "hours").
242
+
243
+ :rtype: str
244
+ :returns: The appropriate date format string based on the provided units.
245
+ """
246
+ if units == 'days':
247
+ return '%Y-%m-%d'
248
+ elif units == 'weeks':
249
+ return '%Y-%W'
250
+ elif units == 'months':
251
+ return '%Y-%m'
252
+ elif units == 'years':
253
+ return '%Y'
254
+ else:
255
+ return '%Y-%m-%d %H:%M'
256
+
257
+
258
+ def check_dates_consecutive(dates, unit):
259
+ """
260
+ Check if the dates in the list are consecutive based on the provided unit.
261
+
262
+ :param list dates: A list of strings representing the dates to be checked.
263
+ :param str unit: The unit to check for consecutiveness ("hours", "days", "weeks", "months", or "years").
264
+
265
+ :rtype: bool
266
+ :returns: True if the dates are consecutive in the given unit, otherwise False.
267
+ """
268
+ if len(dates) == 0:
269
+ return False
270
+
271
+ if len(dates) == 1:
272
+ return True
273
+
274
+ consecutive = False
275
+ format_str = get_format_for_units(unit)
276
+
277
+ for i in range(len(dates) - 1):
278
+ date1 = datetime.strptime(dates[i], format_str)
279
+ date2 = datetime.strptime(dates[i + 1], format_str)
280
+
281
+ if unit == 'hours':
282
+ diff = (date2 - date1).total_seconds() / 3600
283
+ elif unit == 'days':
284
+ diff = (date2 - date1).days
285
+ elif unit == 'weeks':
286
+ diff = (date2 - date1).days // 7
287
+ elif unit == 'months':
288
+ diff = relativedelta(date2, date1).months + (relativedelta(date2, date1).years * 12)
289
+ elif unit == 'years':
290
+ diff = relativedelta(date2, date1).years
291
+ else:
292
+ raise ValueError("Unsupported unit: {}".format(unit))
293
+
294
+ if abs(diff) == 1:
295
+ consecutive = True
296
+ else:
297
+ consecutive = False
298
+ break
299
+
300
+ return consecutive
@@ -0,0 +1,25 @@
1
+ import six
2
+
3
+
4
+ def parse_bool_attribute(attribute):
5
+ """
6
+ Parse a boolean attribute.
7
+ :param attribute:
8
+ :return: bool
9
+ """
10
+ return str(attribute).lower() in ("1", "true")
11
+
12
+
13
+ def replace_entities(text):
14
+ """
15
+ Replace all HTML entities with their respective unicode characters.
16
+ :param text:
17
+ :return:
18
+ """
19
+ if six.PY2:
20
+ from HTMLParser import HTMLParser
21
+ parser = HTMLParser()
22
+ return parser.unescape(text)
23
+ else:
24
+ import html
25
+ return html.unescape(text)
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: ooui
3
+ Version: 0.1.0
4
+ Summary: Open Object User Interface
5
+ Home-page: https://github.com/gisce/python-ooui
6
+ Author: GISCE
7
+ Author-email: devel@gisce.net
8
+ License: MIT
9
+ Provides: ooui
10
+ License-File: LICENSE
11
+
12
+ Open Object User Interface for GISCE-ERP
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ ooui/__init__.py
5
+ ooui.egg-info/PKG-INFO
6
+ ooui.egg-info/SOURCES.txt
7
+ ooui.egg-info/dependency_links.txt
8
+ ooui.egg-info/requires.txt
9
+ ooui.egg-info/top_level.txt
10
+ ooui/graph/__init__.py
11
+ ooui/graph/axis.py
12
+ ooui/graph/base.py
13
+ ooui/graph/chart.py
14
+ ooui/graph/fields.py
15
+ ooui/graph/indicator.py
16
+ ooui/graph/processor.py
17
+ ooui/graph/timerange.py
18
+ ooui/helpers/__init__.py
@@ -0,0 +1,3 @@
1
+ lxml
2
+ python-dateutil
3
+ six
@@ -0,0 +1 @@
1
+ ooui
ooui-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
ooui-0.1.0/setup.py ADDED
@@ -0,0 +1,23 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='ooui',
5
+ description='Open Object User Interface',
6
+ author='GISCE',
7
+ author_email='devel@gisce.net',
8
+ url='https://github.com/gisce/python-ooui',
9
+ version='0.1.0',
10
+ license='MIT',
11
+ long_description='''Open Object User Interface for GISCE-ERP''',
12
+ provides=['ooui'],
13
+ install_requires=[
14
+ 'lxml',
15
+ 'python-dateutil',
16
+ 'six',
17
+ ],
18
+ tests_require=[
19
+ 'mamba',
20
+ 'expects',
21
+ ],
22
+ packages=find_packages()
23
+ )