zepben.ewb 1.1.0b6__py3-none-any.whl → 1.1.0b7__py3-none-any.whl

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.
zepben/ewb/__init__.py CHANGED
@@ -352,6 +352,7 @@ from zepben.ewb.services.diagram.diagram_service_comparator import DiagramServic
352
352
 
353
353
  from zepben.ewb.database.paths.database_type import *
354
354
  from zepben.ewb.database.paths.ewb_data_file_paths import *
355
+ from zepben.ewb.database.paths.local_ewb_data_file_paths import *
355
356
 
356
357
  from zepben.ewb.database.sql.column import *
357
358
  from zepben.ewb.database.sqlite.tables.sqlite_table import *
@@ -1,202 +1,89 @@
1
- # Copyright 2024 Zeppelin Bend Pty Ltd
1
+ # Copyright 2025 Zeppelin Bend Pty Ltd
2
2
  # This Source Code Form is subject to the terms of the Mozilla Public
3
3
  # License, v. 2.0. If a copy of the MPL was not distributed with this
4
4
  # file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
 
6
6
  __all__ = ['EwbDataFilePaths']
7
7
 
8
+ from abc import ABC, abstractmethod
8
9
  from datetime import date, timedelta
9
10
  from pathlib import Path
10
- from typing import Callable, Iterator, Optional, List
11
+ from typing import Optional, List, Generator
11
12
 
12
13
  from zepben.ewb import require
13
14
  from zepben.ewb.database.paths.database_type import DatabaseType
14
15
 
15
16
 
16
- class EwbDataFilePaths:
17
+ class EwbDataFilePaths(ABC):
17
18
  """Provides paths to all the various data files / folders used by EWB."""
18
19
 
19
- def __init__(self, base_dir: Path,
20
- create_path: bool = False,
21
- create_directories_func: Callable[[Path], None] = lambda it: it.mkdir(parents=True),
22
- is_directory: Callable[[Path], bool] = Path.is_dir,
23
- exists: Callable[[Path], bool] = Path.exists,
24
- list_files: Callable[[Path], Iterator[Path]] = Path.iterdir):
25
- """
26
- :param base_dir: The root directory of the EWB data structure.
27
- :param create_path: Create the root directory (and any missing parent folders) if it does not exist.
28
- """
29
- self.create_directories_func = create_directories_func
30
- self.is_directory = is_directory
31
- self.exists = exists
32
- self.list_files = list_files
33
- self._base_dir = base_dir
34
-
35
- if create_path:
36
- self.create_directories_func(base_dir)
37
-
38
- require(self.is_directory(base_dir), lambda: f"base_dir must be a directory")
39
-
40
- @property
41
- def base_dir(self):
42
- """The root directory of the EWB data structure."""
43
- return self._base_dir
44
-
45
- def customer(self, database_date: date) -> Path:
46
- """
47
- Determine the path to the "customers" database for the specified date.
48
-
49
- :param database_date: The :class:`date` to use for the "customers" database.
50
- :return: The :class:`path` to the "customers" database for the specified date.
51
- """
52
- return self._to_dated_path(database_date, DatabaseType.CUSTOMER.file_descriptor)
53
-
54
- def diagram(self, database_date: date) -> Path:
55
- """
56
- Determine the path to the "diagrams" database for the specified date.
57
-
58
- :param database_date: The :class:`date` to use for the "diagrams" database.
59
- :return: The :class:`path` to the "diagrams" database for the specified date.
60
- """
61
- return self._to_dated_path(database_date, DatabaseType.DIAGRAM.file_descriptor)
62
-
63
- def measurement(self, database_date: date) -> Path:
64
- """
65
- Determine the path to the "measurements" database for the specified date.
66
-
67
- :param database_date: The :class:`date` to use for the "measurements" database.
68
- :return: The :class:`path` to the "measurements" database for the specified date.
69
- """
70
- return self._to_dated_path(database_date, DatabaseType.MEASUREMENT.file_descriptor)
71
-
72
- def network_model(self, database_date: date) -> Path:
73
- """
74
- Determine the path to the "network model" database for the specified date.
75
-
76
- :param database_date: The :class:`date` to use for the "network model" database.
77
- :return: The :class:`path` to the "network model" database for the specified date.
78
- """
79
- return self._to_dated_path(database_date, DatabaseType.NETWORK_MODEL.file_descriptor)
80
-
81
- def tile_cache(self, database_date: date) -> Path:
82
- """
83
- Determine the path to the "tile cache" database for the specified date.
84
-
85
- :param database_date: The :class:`date` to use for the "tile cache" database.
86
- :return: The :class:`path` to the "tile cache" database for the specified date.
87
- """
88
- return self._to_dated_path(database_date, DatabaseType.TILE_CACHE.file_descriptor)
89
-
90
- def energy_reading(self, database_date: date) -> Path:
91
- """
92
- Determine the path to the "energy readings" database for the specified date.
93
-
94
- :param database_date: The :class:`date` to use for the "energy readings" database.
95
- :return: The :class:`path` to the "energy readings" database for the specified date.
96
- """
97
- return self._to_dated_path(database_date, DatabaseType.ENERGY_READING.file_descriptor)
98
-
99
- def energy_readings_index(self) -> Path:
100
- """
101
- Determine the path to the "energy readings index" database.
102
-
103
- :return: The :class:`path` to the "energy readings index" database.
104
- """
105
- return self._base_dir.joinpath(f"{DatabaseType.ENERGY_READINGS_INDEX.file_descriptor}.sqlite")
20
+ VARIANTS_PATH: str = "variants"
21
+ """
22
+ The folder containing the variants. Will be placed under the dated folder alongside the network model database.
23
+ """
106
24
 
107
- def load_aggregator_meters_by_date(self) -> Path:
25
+ def resolve(self, database_type: DatabaseType, database_date: Optional[date] = None, variant: Optional[str] = None) -> Path:
108
26
  """
109
- Determine the path to the "load aggregator meters-by-date" database.
27
+ Resolves the :class:`Path` to the database file for the specified :class:`DatabaseType`, within the specified `database_date`
28
+ and optional `variant` when `DatabaseType.per_date` is set to true.
110
29
 
111
- :return: The :class:`path` to the "load aggregator meters-by-date" database.
112
- """
113
- return self._base_dir.joinpath(f"{DatabaseType.LOAD_AGGREGATOR_METERS_BY_DATE.file_descriptor}.sqlite")
114
-
115
- def weather_reading(self) -> Path:
116
- """
117
- Determine the path to the "weather readings" database.
30
+ :param database_type: The :class:`DatabaseType` to use for the database :class:`Path`.
31
+ :param database_date: The :class:`date` to use for the database :class:`Path`. Required when `database_type.per_date` is true, otherwise must be `None`.
32
+ :param variant: The optional name of the variant containing the database.
118
33
 
119
- :return: The :class:`path` to the "weather readings" database.
34
+ :return: The :class:`Path` to the :class:`DatabaseType` database file.
120
35
  """
121
- return self._base_dir.joinpath(f"{DatabaseType.WEATHER_READING.file_descriptor}.sqlite")
122
-
123
- def results_cache(self) -> Path:
124
- """
125
- Determine the path to the "results cache" database.
126
-
127
- :return: The :class:`path` to the "results cache" database.
128
- """
129
- return self._base_dir.joinpath(f"{DatabaseType.RESULTS_CACHE.file_descriptor}.sqlite")
36
+ if database_date is not None:
37
+ require(database_type.per_date, lambda: "database_type must have its per_date set to True to use this method with a database_date.")
38
+ if variant is not None:
39
+ return self.resolve_database(self._to_dated_variant_path(database_type, database_date, variant))
40
+ else:
41
+ return self.resolve_database(self._to_dated_path(database_type, database_date))
42
+ else:
43
+ require(not database_type.per_date, lambda: "database_type must have its per_date set to False to use this method without a database_date.")
44
+ return self.resolve_database(Path(self._database_name(database_type)))
130
45
 
46
+ @abstractmethod
131
47
  def create_directories(self, database_date: date) -> Path:
132
48
  """
133
49
  Create the directories required to have a valid path for the specified date.
134
50
 
135
51
  :param database_date: The :class:`date` required in the path.
136
- :return: The :class:`path` to the directory for the `database_date`.
137
- """
138
- date_path = self._base_dir.joinpath(str(database_date))
139
- if self.exists(date_path):
140
- return date_path
141
- else:
142
- self.create_directories_func(date_path)
143
- return date_path
144
-
145
- def _to_dated_path(self, database_date: date, file: str) -> Path:
146
- return self._base_dir.joinpath(str(database_date), f"{database_date}-{file}.sqlite")
147
-
148
- def _check_exists(self, database_type: DatabaseType, database_date: date) -> bool:
52
+ :return: The :class:`Path` to the directory for the `database_date`.
149
53
  """
150
- Check if a database of the specified type and date exists.
54
+ raise NotImplemented
151
55
 
152
- :param database_type: The type of database to search for.
153
- :param database_date: The date to check.
154
- :return: `True` if a database of the specified `database_type` and `database_date` exists in the date path.
155
- """
156
- if not database_type.per_date:
157
- raise ValueError("INTERNAL ERROR: Should only be calling `checkExists` for `perDate` files.")
158
-
159
- if database_type == DatabaseType.CUSTOMER:
160
- model_path = self.customer(database_date)
161
- elif database_type == DatabaseType.DIAGRAM:
162
- model_path = self.diagram(database_date)
163
- elif database_type == DatabaseType.MEASUREMENT:
164
- model_path = self.measurement(database_date)
165
- elif database_type == DatabaseType.NETWORK_MODEL:
166
- model_path = self.network_model(database_date)
167
- elif database_type == DatabaseType.TILE_CACHE:
168
- model_path = self.tile_cache(database_date)
169
- elif database_type == DatabaseType.ENERGY_READING:
170
- model_path = self.energy_reading(database_date)
171
- else:
172
- raise ValueError(
173
- "INTERNAL ERROR: Should only be calling `check_exists` for `per_date` files, which should all be covered above, so go ahead and add it.")
174
- return self.exists(model_path)
175
-
176
- def find_closest(self, database_type: DatabaseType, max_days_to_search: int = 999, target_date: date = date.today(), search_forwards: bool = False) -> \
177
- Optional[date]:
56
+ def find_closest(
57
+ self,
58
+ database_type: DatabaseType,
59
+ max_days_to_search: int = 999999,
60
+ target_date: date = date.today(),
61
+ search_forwards: bool = False
62
+ ) -> Optional[date]:
178
63
  """
179
64
  Find the closest date with a usable database of the specified type.
180
65
 
181
66
  :param database_type: The type of database to search for.
182
67
  :param max_days_to_search: The maximum number of days to search for a valid database.
183
- :param target_date: The target :class:`date`. Defaults to today.
184
- :param search_forwards: Indicates the search should also look forwards in time from `start_date` for a valid file. Defaults to reverse search only.
185
- :return: The closest :class:`date` to `database_date` with a valid database of `database_type` within the search parameters, or `None` if no valid database was found.
68
+ :param target_date: The target date. Defaults to today.
69
+ :param search_forwards: Indicates the search should also look forwards in time from `target_date` for a valid file. Defaults to reverse search only.
70
+
71
+ :return: The closest :class:`date` to `target_date` with a valid database of `database_type` within the search parameters, or null if no valid database
72
+ was found.
186
73
  """
187
74
  if not database_type.per_date:
188
75
  return None
189
76
 
190
- if self._check_exists(database_type, target_date):
77
+ descendants = list(self.enumerate_descendants())
78
+ if self._check_exists(descendants, database_type, target_date):
191
79
  return target_date
192
80
 
193
81
  offset = 1
194
-
195
82
  while offset <= max_days_to_search:
196
83
  offset_days = timedelta(offset)
197
84
  try:
198
85
  previous_date = target_date - offset_days
199
- if self._check_exists(database_type, previous_date):
86
+ if self._check_exists(descendants, database_type, previous_date):
200
87
  return previous_date
201
88
  except OverflowError:
202
89
  pass
@@ -204,34 +91,102 @@ class EwbDataFilePaths:
204
91
  if search_forwards:
205
92
  try:
206
93
  forward_date = target_date + offset_days
207
- if self._check_exists(database_type, forward_date):
94
+ if self._check_exists(descendants, database_type, forward_date):
208
95
  return forward_date
209
96
  except OverflowError:
210
97
  pass
98
+
211
99
  offset += 1
100
+
212
101
  return None
213
102
 
214
- def _get_available_dates_for(self, database_type: DatabaseType) -> List[date]:
103
+ def get_available_dates_for(self, database_type: DatabaseType) -> List[date]:
104
+ """
105
+ Find available databases specified by :class:`DatabaseType` in data path.
106
+
107
+ :param database_type: The type of database to search for.
108
+
109
+ :return: list of :class:`date`'s for which this specified :class:`DatabaseType` databases exist in the data path.
110
+ """
215
111
  if not database_type.per_date:
216
112
  raise ValueError(
217
- "INTERNAL ERROR: Should only be calling `_get_available_dates_for` for `per_date` files.")
113
+ "INTERNAL ERROR: Should only be calling `get_available_dates_for` for `per_date` files, "
114
+ "which should all be covered above, so go ahead and add it."
115
+ )
218
116
 
219
117
  to_return = list()
220
118
 
221
- for file in self.list_files(self._base_dir):
222
- if self.is_directory(file):
119
+ for it in self.enumerate_descendants():
120
+ if it.name.endswith(self._database_name(database_type)):
223
121
  try:
224
- database_date = date.fromisoformat(file.name)
225
- if self.exists(self._to_dated_path(database_date, database_type.file_descriptor)):
226
- to_return.append(database_date)
122
+ to_return.append(date.fromisoformat(it.parent.name))
227
123
  except ValueError:
228
124
  pass
125
+
126
+ return sorted(to_return)
127
+
128
+ def get_available_variants_for(self, target_date: date = date.today()) -> List[str]:
129
+ """
130
+ Find available variants for the specified `target_date` in data path.
131
+
132
+ :param target_date: The target date. Defaults to today.
133
+
134
+ :return: list of variant names that exist in the data path for the specified `target_date`.
135
+ """
136
+ to_return = list()
137
+
138
+ for it in self.enumerate_descendants():
139
+ try:
140
+ if (str(it.parent.name).lower() == self.VARIANTS_PATH) and (str(it.parent.parent.name) == str(target_date)):
141
+ to_return.append(str(it.name))
142
+ except ValueError:
143
+ pass
144
+
229
145
  return sorted(to_return)
230
146
 
231
- def get_network_model_databases(self) -> List[date]:
147
+ @abstractmethod
148
+ def enumerate_descendants(self) -> Generator[Path, None, None]:
149
+ """
150
+ Lists the child items of source location.
151
+
152
+ :return: generator of child items.
153
+ """
154
+ raise NotImplemented
155
+
156
+ @abstractmethod
157
+ def resolve_database(self, path: Path) -> Path:
232
158
  """
233
- Find available network-model databases in data path.
159
+ Resolves the database in the specified source :class:`Path`.
234
160
 
235
- :return: A list of :class:`date`'s for which network-model databases exist in the data path.
161
+ :param path: :class:`Path` to the source database file.
162
+ :return: :class:`Path` to the local database file.
236
163
  """
237
- return self._get_available_dates_for(DatabaseType.NETWORK_MODEL)
164
+ raise NotImplemented
165
+
166
+ def _check_exists(self, descendants: List[Path], database_type: DatabaseType, database_date: date) -> bool:
167
+ """
168
+ Check if a database :class:`Path` of the specified :class:`DatabaseType` and :class:`date` exists.
169
+
170
+ :param descendants: A list of :class:`Path` representing the descendant paths.
171
+ :param database_type: The type of database to search for.
172
+ :param database_date: The date to check.
173
+
174
+ :return: True if a database of the specified `database_type` and `database_date` exits in the date path.
175
+ """
176
+ for cp in descendants:
177
+ if cp.is_relative_to(self._to_dated_path(database_type, database_date)):
178
+ return True
179
+
180
+ return False
181
+
182
+ def _to_dated_path(self, database_type: DatabaseType, database_date: date) -> Path:
183
+ date_str = str(database_date)
184
+ return Path(date_str).joinpath(f"{date_str}-{self._database_name(database_type)}")
185
+
186
+ def _to_dated_variant_path(self, database_type: DatabaseType, database_date: date, variant: str) -> Path:
187
+ date_str = str(database_date)
188
+ return Path(date_str).joinpath(self.VARIANTS_PATH, variant, f"{date_str}-{self._database_name(database_type)}")
189
+
190
+ @staticmethod
191
+ def _database_name(database_type: DatabaseType) -> str:
192
+ return f"{database_type.file_descriptor}.sqlite"
@@ -0,0 +1,58 @@
1
+ # Copyright 2025 Zeppelin Bend Pty Ltd
2
+ # This Source Code Form is subject to the terms of the Mozilla Public
3
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+
6
+ __all__ = ['LocalEwbDataFilePaths']
7
+
8
+ from datetime import date
9
+ from pathlib import Path
10
+ from typing import Callable, Generator, Union
11
+
12
+ from zepben.ewb import require
13
+ from zepben.ewb.database.paths.ewb_data_file_paths import EwbDataFilePaths
14
+
15
+
16
+ class LocalEwbDataFilePaths(EwbDataFilePaths):
17
+ """Provides paths to all the various data files / folders in the local file system used by EWB."""
18
+
19
+ def __init__(
20
+ self,
21
+ base_dir: Union[Path, str],
22
+ create_path: bool = False,
23
+ create_directories_func: Callable[[Path], None] = lambda it: it.mkdir(parents=True),
24
+ is_directory: Callable[[Path], bool] = Path.is_dir,
25
+ exists: Callable[[Path], bool] = Path.exists,
26
+ list_files: Callable[[Path], Generator[Path, None, None]] = Path.iterdir,
27
+ ):
28
+ """
29
+ :param base_dir: The root directory of the EWB data structure.
30
+ :param create_path: Create the root directory (and any missing parent folders) if it does not exist.
31
+ :param create_directories_func: Function for directory creation.
32
+ :param is_directory: Function to determine if the supplied path is a directory .
33
+ :param exists: Function to determine if the supplied path exists.
34
+ :param list_files: Function for listing directories and files under the supplied path.
35
+ """
36
+ self._base_dir = Path(base_dir)
37
+ self._create_directories_func = create_directories_func
38
+ self._exists = exists
39
+ self._list_files = list_files
40
+
41
+ if create_path:
42
+ self._create_directories_func(base_dir)
43
+
44
+ require(is_directory(base_dir), lambda: f"base_dir must be a directory")
45
+
46
+ def create_directories(self, database_date: date) -> Path:
47
+ date_path = self._base_dir.joinpath(str(database_date))
48
+ if not self._exists(date_path):
49
+ self._create_directories_func(date_path)
50
+
51
+ return date_path
52
+
53
+ def enumerate_descendants(self) -> Generator[Path, None, None]:
54
+ for it in self._list_files(self._base_dir):
55
+ yield it
56
+
57
+ def resolve_database(self, path: Path) -> Path:
58
+ return self._base_dir.joinpath(path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zepben.ewb
3
- Version: 1.1.0b6
3
+ Version: 1.1.0b7
4
4
  Summary: Python SDK for interacting with the Energy Workbench platform
5
5
  Author-email: Kurt Greaves <kurt.greaves@zepben.com>, Max Chesterfield <max.chesterfield@zepben.com>
6
6
  License-Expression: MPL-2.0
@@ -1,4 +1,4 @@
1
- zepben/ewb/__init__.py,sha256=B5sJdP6ozOagFMdAD8AYfcqAqicoHjug28IiRS9FYp8,40514
1
+ zepben/ewb/__init__.py,sha256=nPkN55KJGBRGRk7fLZAFmzaxu9Z7RR7gGAAwFWdy3lY,40580
2
2
  zepben/ewb/exceptions.py,sha256=KLR6_5U-K4_VtKQZkKBlbOF7wlKnajQuhSiGeeNAo9U,1384
3
3
  zepben/ewb/types.py,sha256=067jjQX6eCbgaEtlQPdSBi_w4_16unbP1f_g5NrVj_w,627
4
4
  zepben/ewb/util.py,sha256=lgxqbGEQO8df-Bs88-4bVijB8CY9rY8ox8WjibqZWnM,5728
@@ -12,7 +12,8 @@ zepben/ewb/auth/common/auth_provider_config.py,sha256=-MkmrOXFgp2K-Q9f-oyaNr2Qhe
12
12
  zepben/ewb/database/__init__.py,sha256=waADXEvfUG9wAN4STx5uIUHOv0UnpZLH2qU1LXgaDBc,243
13
13
  zepben/ewb/database/paths/__init__.py,sha256=waADXEvfUG9wAN4STx5uIUHOv0UnpZLH2qU1LXgaDBc,243
14
14
  zepben/ewb/database/paths/database_type.py,sha256=6l43Q87n3pQ4ClG9zfbp7cRSjt-Fxb01Tdrt48ZHMbk,1025
15
- zepben/ewb/database/paths/ewb_data_file_paths.py,sha256=hoED3sNmnB7tQF9dMPcCH9ozOa5P85u_RewZjqsPMbs,10426
15
+ zepben/ewb/database/paths/ewb_data_file_paths.py,sha256=MchDxklK9atSxOUtq28Yi4hRyilVk2GF8deMrsMdBt4,8063
16
+ zepben/ewb/database/paths/local_ewb_data_file_paths.py,sha256=XeOzoTvuaheD6KAM6EGW56rkiSDHIPE-x7_8iP5vQR0,2390
16
17
  zepben/ewb/database/sql/__init__.py,sha256=8-znO960twGtcAGArLGl_ijbCB9BBv0_hUNYf1eF0Lk,243
17
18
  zepben/ewb/database/sql/column.py,sha256=migPYL0in18cCWd3H51M-op2uqJeYqGv9KIksSiv75s,1049
18
19
  zepben/ewb/database/sql/sql_table.py,sha256=0ADKRb_2uAz0iSz52wbwOjovctvwRte8sHBBfEGj7cU,5235
@@ -634,8 +635,8 @@ zepben/ewb/streaming/mutations/update_network_state_client.py,sha256=e0Oma5PRT8m
634
635
  zepben/ewb/streaming/mutations/update_network_state_service.py,sha256=irR-TO67QXRyBmK8PU8SzM31NKSSefZt_nQGHi5IhT8,3260
635
636
  zepben/ewb/testing/__init__.py,sha256=waADXEvfUG9wAN4STx5uIUHOv0UnpZLH2qU1LXgaDBc,243
636
637
  zepben/ewb/testing/test_network_builder.py,sha256=KG0o2ZHUswx3xClu-JnLs_pYIYbQ5jjtvtyZ7LI6IZ8,38092
637
- zepben_ewb-1.1.0b6.dist-info/licenses/LICENSE,sha256=aAHD66h6PQIETpkJDvg5yEObyFvXUED8u7S8dlh6K0Y,16725
638
- zepben_ewb-1.1.0b6.dist-info/METADATA,sha256=UyEQUhwT3LJSz7nRPfnbA8wdmOsiwXjzQoPkbgzLe3Y,3232
639
- zepben_ewb-1.1.0b6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
640
- zepben_ewb-1.1.0b6.dist-info/top_level.txt,sha256=eVLDJiO6FGjL_Z7KdmFE-R8uf1Q07aaVLGe9Ee4kmBw,7
641
- zepben_ewb-1.1.0b6.dist-info/RECORD,,
638
+ zepben_ewb-1.1.0b7.dist-info/licenses/LICENSE,sha256=aAHD66h6PQIETpkJDvg5yEObyFvXUED8u7S8dlh6K0Y,16725
639
+ zepben_ewb-1.1.0b7.dist-info/METADATA,sha256=1hwk8UacCWwT38sTK6gL_4SsQa8CXQGNQmoUMBKhz3E,3232
640
+ zepben_ewb-1.1.0b7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
641
+ zepben_ewb-1.1.0b7.dist-info/top_level.txt,sha256=eVLDJiO6FGjL_Z7KdmFE-R8uf1Q07aaVLGe9Ee4kmBw,7
642
+ zepben_ewb-1.1.0b7.dist-info/RECORD,,