apyefa 0.0.1__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.

Potentially problematic release.


This version of apyefa might be problematic. Click here for more details.

apyefa/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from apyefa.client import EfaClient
2
+ from apyefa.data_classes import (
3
+ Departure,
4
+ Line,
5
+ Location,
6
+ LocationFilter,
7
+ LocationType,
8
+ SystemInfo,
9
+ TransportType,
10
+ )
11
+
12
+ __all__ = [
13
+ "LocationFilter",
14
+ "Location",
15
+ "LocationType",
16
+ "Departure",
17
+ "SystemInfo",
18
+ "Line",
19
+ "TransportType",
20
+ "EfaClient",
21
+ ]
apyefa/client.py ADDED
@@ -0,0 +1,215 @@
1
+ import logging
2
+
3
+ import aiohttp
4
+
5
+ from apyefa.commands import (
6
+ Command,
7
+ CommandDepartures,
8
+ CommandServingLines,
9
+ CommandStopFinder,
10
+ CommandSystemInfo,
11
+ )
12
+ from apyefa.data_classes import (
13
+ CoordFormat,
14
+ Departure,
15
+ Line,
16
+ Location,
17
+ LocationFilter,
18
+ LocationType,
19
+ SystemInfo,
20
+ )
21
+ from apyefa.exceptions import EfaConnectionError
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class EfaClient:
27
+ async def __aenter__(self):
28
+ self._client_session = aiohttp.ClientSession()
29
+ return self
30
+
31
+ async def __aexit__(self, *args, **kwargs):
32
+ await self._client_session.__aexit__(*args, **kwargs)
33
+
34
+ def __init__(self, url: str, debug: bool = False):
35
+ """Create a new instance of client.
36
+
37
+ Args:
38
+ url (str): url string to EFA endpoint
39
+
40
+ Raises:
41
+ ValueError: No url provided
42
+ """
43
+ if not url:
44
+ raise ValueError("No EFA endpoint url provided")
45
+
46
+ self._debug: bool = debug
47
+ self._base_url: str = url if url.endswith("/") else f"{url}/"
48
+
49
+ async def info(self) -> SystemInfo:
50
+ """Get system info used by EFA endpoint.
51
+
52
+ Returns:
53
+ SystemInfo: info object
54
+ """
55
+ _LOGGER.info("Request system info")
56
+
57
+ command = CommandSystemInfo()
58
+ response = await self._run_query(self._build_url(command))
59
+
60
+ return command.parse(response)
61
+
62
+ async def locations_by_name(
63
+ self, name: str, *, filters: list[LocationFilter] = [], limit: int = 30
64
+ ) -> list[Location]:
65
+ """Find location(s) by provided `name`.
66
+
67
+ Args:
68
+ name (str): Name or ID of location to search (case insensitive)
69
+ e.g. "Plärrer", "Nordostbanhof" or "de:09564:704"
70
+ filters (list[LocationFilter], optional): List of filters to apply for search. Defaults to empty.
71
+ limit (int, optional): Max size of returned list. Defaults to 30.
72
+
73
+ Returns:
74
+ list[Location]: List of location(s) returned by endpoint. List is sorted by match quality.
75
+ """
76
+ _LOGGER.info(f"Request location search by name/id: {name}")
77
+ _LOGGER.debug(f"filters: {filters}")
78
+ _LOGGER.debug(f"limit: {limit}")
79
+
80
+ command = CommandStopFinder("any", name)
81
+ command.add_param("anyMaxSizeHitList", limit)
82
+
83
+ if filters:
84
+ command.add_param("anyObjFilter_sf", sum(filters))
85
+
86
+ response = await self._run_query(self._build_url(command))
87
+
88
+ return command.parse(response)
89
+
90
+ async def location_by_coord(
91
+ self,
92
+ coord_x: float,
93
+ coord_y: float,
94
+ format: CoordFormat = CoordFormat.WGS84,
95
+ limit: int = 10,
96
+ ) -> Location:
97
+ """Find location(s) by provided `coordinates`.
98
+
99
+ Args:
100
+ coord_x (float): X coordinate
101
+ coord_y (float): Y coordinate
102
+ format (CoordFormat, optional): Coordinate format. Defaults to CoordFormat.WGS84.
103
+ limit (int, optional): Max size of returned list. Defaults to 10.
104
+
105
+ Returns:
106
+ Location: List of location(s) returned by endpoint. List is sorted by match quality.
107
+ """
108
+ _LOGGER.info("Request location search by coordinates")
109
+ _LOGGER.debug(f"coord_x: {coord_x}")
110
+ _LOGGER.debug(f"coord_y: {coord_y}")
111
+ _LOGGER.debug(f"format: {format}")
112
+ _LOGGER.debug(f"limit: {limit}")
113
+
114
+ command = CommandStopFinder("coord", f"{coord_x}:{coord_y}:{format}")
115
+ command.add_param("anyMaxSizeHitList", limit)
116
+
117
+ response = await self._run_query(self._build_url(command))
118
+
119
+ return command.parse(response)
120
+
121
+ async def trip(self):
122
+ raise NotImplementedError
123
+
124
+ async def departures_by_location(
125
+ self,
126
+ stop: Location | str,
127
+ limit=40,
128
+ date: str | None = None,
129
+ ) -> list[Departure]:
130
+ _LOGGER.info(f"Request departures for stop {stop}")
131
+ _LOGGER.debug(f"limit: {limit}")
132
+ _LOGGER.debug(f"date: {date}")
133
+
134
+ if isinstance(stop, Location):
135
+ stop = stop.id
136
+
137
+ command = CommandDepartures(stop)
138
+
139
+ # add parameters
140
+ command.add_param("limit", limit)
141
+ command.add_param_datetime(date)
142
+
143
+ response = await self._run_query(self._build_url(command))
144
+
145
+ return command.parse(response)
146
+
147
+ async def lines_by_name(self, line: str) -> list[Line]:
148
+ """Search lines by name. e.g. subway `U3` or bus `65`
149
+
150
+ Args:
151
+ line (str): Line name to search
152
+
153
+ Returns:
154
+ list[Transport]: List of lines
155
+ """
156
+ _LOGGER.info("Request lines by name")
157
+ _LOGGER.debug(f"line:{line}")
158
+
159
+ command = CommandServingLines("line", line)
160
+
161
+ response = await self._run_query(self._build_url(command))
162
+
163
+ return command.parse(response)
164
+
165
+ async def lines_by_location(self, location: str | Location) -> list[Line]:
166
+ """Search for lines that pass `location`. Location can be location ID like `de:08111:6221` or a Location object
167
+
168
+ Args:
169
+ location (str | Location): Location
170
+
171
+ Raises:
172
+ ValueError: If not a stop location provided but e.g. POI or Address
173
+
174
+ Returns:
175
+ list[Transport]: List of lines
176
+ """
177
+ _LOGGER.info("Request lines by location")
178
+ _LOGGER.debug(f"location:{location}")
179
+
180
+ if isinstance(location, Location):
181
+ if location.loc_type != LocationType.STOP:
182
+ raise ValueError(
183
+ f"Only locations with type Stop are supported, provided {location.loc_type}"
184
+ )
185
+ location = location.id
186
+
187
+ command = CommandServingLines("odv", location)
188
+
189
+ response = await self._run_query(self._build_url(command))
190
+
191
+ return command.parse(response)
192
+
193
+ async def locations_by_line(self, line: str | Line) -> list[Location]:
194
+ raise NotImplementedError
195
+
196
+ async def _run_query(self, query: str) -> str:
197
+ _LOGGER.info(f"Run query {query}")
198
+
199
+ async with self._client_session.get(query, ssl=False) as response:
200
+ _LOGGER.debug(f"Response status: {response.status}")
201
+
202
+ if response.status == 200:
203
+ text = await response.text()
204
+
205
+ if self._debug:
206
+ _LOGGER.debug(text)
207
+
208
+ return text
209
+ else:
210
+ raise EfaConnectionError(
211
+ f"Failed to fetch data from endpoint. Returned status: {response.status}"
212
+ )
213
+
214
+ def _build_url(self, cmd: Command):
215
+ return self._base_url + str(cmd)
@@ -0,0 +1,15 @@
1
+ from .command import Command
2
+ from .command_departures import CommandDepartures
3
+ from .command_serving_lines import CommandServingLines
4
+ from .command_stop_finder import CommandStopFinder
5
+ from .command_system_info import CommandSystemInfo
6
+ from .command_trip import CommandTrip
7
+
8
+ __all__ = [
9
+ "Command",
10
+ "CommandDepartures",
11
+ "CommandStopFinder",
12
+ "CommandSystemInfo",
13
+ "CommandTrip",
14
+ "CommandServingLines",
15
+ ]
@@ -0,0 +1,115 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+
4
+ from voluptuous import MultipleInvalid, Schema
5
+
6
+ from apyefa.commands.parsers.rapid_json_parser import RapidJsonParser
7
+ from apyefa.exceptions import EfaFormatNotSupported, EfaParameterError
8
+ from apyefa.helpers import is_date, is_datetime, is_time
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class Command:
14
+ def __init__(self, name: str, macro: str, output_format: str = "rapidJSON") -> None:
15
+ self._name: str = name
16
+ self._macro: str = macro
17
+ self._parameters: dict[str, str] = {}
18
+ self._format: str = output_format
19
+
20
+ self.add_param("outputFormat", output_format)
21
+ self.add_param("coordOutputFormat", "WGS84")
22
+
23
+ def add_param(self, param: str, value: str):
24
+ if not param or not value:
25
+ return
26
+
27
+ if param not in self._get_params_schema().schema.keys():
28
+ raise EfaParameterError(
29
+ f"Parameter {param} is now allowed for this command"
30
+ )
31
+
32
+ _LOGGER.debug(f'Add parameter "{param}" with value "{value}"')
33
+
34
+ self._parameters.update({param: value})
35
+
36
+ _LOGGER.debug("Updated parameters:")
37
+ _LOGGER.debug(self._parameters)
38
+
39
+ def add_param_datetime(self, date: str):
40
+ if not date:
41
+ return
42
+
43
+ if is_datetime(date):
44
+ self.add_param("itdDate", date.split(" ")[0])
45
+ self.add_param("itdTime", date.split(" ")[1].replace(":", ""))
46
+ elif is_date(date):
47
+ self.add_param("itdDate", date)
48
+ elif is_time(date):
49
+ self.add_param("itdTime", date.replace(":", ""))
50
+ else:
51
+ raise ValueError("Date(time) provided in invalid format")
52
+
53
+ def to_str(self) -> str:
54
+ self._parameters = self.extend_with_defaults()
55
+ self.validate()
56
+
57
+ return f"{self._name}?commonMacro={self._macro}" + self._get_params_as_str()
58
+
59
+ def __str__(self) -> str:
60
+ return self.to_str()
61
+
62
+ def validate(self):
63
+ """Validate self._parameters
64
+
65
+ Raises:
66
+ EfaParameterError: some of parameters are missing or have invalid values
67
+ """
68
+ params_schema = self._get_params_schema()
69
+
70
+ try:
71
+ params = self.extend_with_defaults()
72
+ params_schema(params)
73
+ except MultipleInvalid as exc:
74
+ _LOGGER.error("Parameters validation failed", exc_info=exc)
75
+ raise EfaParameterError(str(exc)) from exc
76
+
77
+ def extend_with_defaults(self) -> dict:
78
+ """Extend self._parameters with default values
79
+
80
+ Returns:
81
+ dict: parameters extended with default values
82
+ """
83
+
84
+ params_schema = self._get_params_schema()
85
+
86
+ return params_schema(self._parameters)
87
+
88
+ def _get_params_as_str(self) -> str:
89
+ """Return parameters concatenated with &
90
+
91
+ Returns:
92
+ str: parameters as string
93
+ """
94
+ if not self._parameters:
95
+ return ""
96
+
97
+ return "&" + "&".join([f"{k}={str(v)}" for k, v in self._parameters.items()])
98
+
99
+ @abstractmethod
100
+ def parse(self, data: str):
101
+ raise NotImplementedError("Abstract method not implemented")
102
+
103
+ @abstractmethod
104
+ def _get_params_schema(self) -> Schema:
105
+ raise NotImplementedError("Abstract method not implemented")
106
+
107
+ @abstractmethod
108
+ def _get_parser(self):
109
+ match self._format:
110
+ case "rapidJSON":
111
+ return RapidJsonParser()
112
+ case _:
113
+ raise EfaFormatNotSupported(
114
+ f"Output format {self._format} is not supported"
115
+ )
@@ -0,0 +1,49 @@
1
+ import logging
2
+
3
+ from voluptuous import Any, Date, Datetime, Optional, Required, Schema
4
+
5
+ from apyefa.commands.command import Command
6
+ from apyefa.data_classes import Departure
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ class CommandDepartures(Command):
12
+ def __init__(self, stop: str) -> None:
13
+ super().__init__("XML_DM_REQUEST", "dm")
14
+
15
+ self.add_param("name_dm", stop)
16
+
17
+ def parse(self, data: dict):
18
+ data = self._get_parser().parse(data)
19
+
20
+ departures = data.get("stopEvents", [])
21
+
22
+ _LOGGER.info(f"{len(departures)} departure(s) found")
23
+
24
+ result = []
25
+
26
+ for departure in departures:
27
+ result.append(Departure.from_dict(departure))
28
+
29
+ return result
30
+
31
+ def _get_params_schema(self) -> Schema:
32
+ return Schema(
33
+ {
34
+ Required("outputFormat", default="rapidJSON"): Any("rapidJSON"),
35
+ Required("coordOutputFormat", default="WGS84"): Any("WGS84"),
36
+ Required("name_dm"): str,
37
+ Required("type_dm", default="stop"): Any("any", "stop"),
38
+ Required("mode", default="direct"): Any("any", "direct"),
39
+ Optional("itdTime"): Datetime("%M%S"),
40
+ Optional("itdDate"): Date("%Y%m%d"),
41
+ Optional("useAllStops"): Any("0", "1", 0, 1),
42
+ Optional("useRealtime", default=1): Any("0", "1", 0, 1),
43
+ Optional("lsShowTrainsExplicit"): Any("0", "1", 0, 1),
44
+ Optional("useProxFootSearch"): Any("0", "1", 0, 1),
45
+ Optional("deleteAssigendStops_dm"): Any("0", "1", 0, 1),
46
+ Optional("doNotSearchForStops_dm"): Any("0", "1", 0, 1),
47
+ Optional("limit"): int,
48
+ }
49
+ )
@@ -0,0 +1,60 @@
1
+ import logging
2
+
3
+ from voluptuous import Any, Optional, Required, Schema
4
+
5
+ from apyefa.commands.command import Command
6
+ from apyefa.data_classes import Line
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ class CommandServingLines(Command):
12
+ def __init__(self, mode: str, value: str) -> None:
13
+ super().__init__("XML_SERVINGLINES_REQUEST", "servingLines")
14
+
15
+ match mode:
16
+ case "odv":
17
+ self.add_param("type_sl", "stopID")
18
+ self.add_param("name_sl", value)
19
+ case "line":
20
+ self.add_param("lineName", value)
21
+ case _:
22
+ raise ValueError(f"Mode {mode} not supported for serving lines")
23
+
24
+ self.add_param("mode", mode)
25
+
26
+ def parse(self, data: dict) -> list[Line]:
27
+ data = self._get_parser().parse(data)
28
+
29
+ transportations = data.get("lines", [])
30
+
31
+ _LOGGER.info(f"{len(transportations)} transportation(s) found")
32
+
33
+ result = []
34
+
35
+ for t in transportations:
36
+ result.append(Line.from_dict(t))
37
+
38
+ return result
39
+
40
+ def _get_params_schema(self) -> Schema:
41
+ return Schema(
42
+ {
43
+ Required("outputFormat", default="rapidJSON"): Any("rapidJSON"),
44
+ Required("coordOutputFormat", default="WGS84"): Any("WGS84"),
45
+ Required("mode", default="line"): Any("odv", "line"),
46
+ # mode 'odv'
47
+ Optional("type_sl"): Any("stopID"),
48
+ Optional("name_sl"): str,
49
+ # mode 'line'
50
+ Optional("lineName"): str,
51
+ Optional("lineReqType"): int,
52
+ Optional("mergeDir"): Any("0", "1", 0, 1),
53
+ Optional("lsShowTrainsExplicit"): Any("0", "1", 0, 1),
54
+ Optional("line"): str,
55
+ # Optional("doNotSearchForStops_sf"): Any("0", "1", 0, 1),
56
+ # Optional("anyObjFilter_origin"): Range(
57
+ # min=0, max=sum([x.value for x in StopFilter])
58
+ # ),
59
+ }
60
+ )
@@ -0,0 +1,48 @@
1
+ import logging
2
+
3
+ from voluptuous import Any, Optional, Range, Required, Schema
4
+
5
+ from apyefa.commands.command import Command
6
+ from apyefa.data_classes import Location, LocationFilter
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ class CommandStopFinder(Command):
12
+ def __init__(self, req_type: str, name: str) -> None:
13
+ super().__init__("XML_STOPFINDER_REQUEST", "stopfinder")
14
+
15
+ self.add_param("type_sf", req_type)
16
+ self.add_param("name_sf", name)
17
+
18
+ def parse(self, data: dict) -> list[Location]:
19
+ data = self._get_parser().parse(data)
20
+
21
+ locations = data.get("locations", [])
22
+
23
+ _LOGGER.info(f"{len(locations)} location(s) found")
24
+
25
+ result = []
26
+
27
+ for location in locations:
28
+ result.append(Location.from_dict(location))
29
+
30
+ return sorted(result, key=lambda x: x.match_quality, reverse=True)
31
+
32
+ def _get_params_schema(self) -> Schema:
33
+ return Schema(
34
+ {
35
+ Required("outputFormat", default="rapidJSON"): Any("rapidJSON"),
36
+ Required("coordOutputFormat", default="WGS84"): Any("WGS84"),
37
+ Required("type_sf", default="any"): Any("any", "coord"),
38
+ Required("name_sf"): str,
39
+ Optional("anyMaxSizeHitList"): int,
40
+ Optional("anySigWhenPerfectNoOtherMatches"): Any("0", "1", 0, 1),
41
+ Optional("anyResSort_sf"): str,
42
+ Optional("anyObjFilter_sf"): int,
43
+ Optional("doNotSearchForStops_sf"): Any("0", "1", 0, 1),
44
+ Optional("anyObjFilter_origin"): Range(
45
+ min=0, max=sum([x.value for x in LocationFilter])
46
+ ),
47
+ }
48
+ )
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ from voluptuous import Any, Optional, Required, Schema
4
+
5
+ from apyefa.commands.command import Command
6
+ from apyefa.data_classes import SystemInfo
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+
11
+ class CommandSystemInfo(Command):
12
+ def __init__(self) -> None:
13
+ super().__init__("XML_SYSTEMINFO_REQUEST", "system")
14
+
15
+ def parse(self, data: dict) -> SystemInfo:
16
+ _LOGGER.info("Parsing system info response")
17
+
18
+ data = self._get_parser().parse(data)
19
+
20
+ return SystemInfo.from_dict(data)
21
+
22
+ def _get_params_schema(self) -> Schema:
23
+ return Schema(
24
+ {
25
+ Required("outputFormat", default="rapidJSON"): Any("rapidJSON"),
26
+ Optional("coordOutputFormat", default="WGS84"): Any("WGS84"),
27
+ }
28
+ )
@@ -0,0 +1,31 @@
1
+ import logging
2
+
3
+ from voluptuous import Any, Optional, Required, Schema
4
+
5
+ from apyefa.commands.command import Command
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+
10
+ class CommandTrip(Command):
11
+ def __init__(self) -> None:
12
+ super().__init__("XML_TRIP_REQUEST2", "trip")
13
+
14
+ def parse(self, data: dict):
15
+ raise NotImplementedError
16
+
17
+ def _get_params_schema(self) -> Schema:
18
+ return Schema(
19
+ {
20
+ Required("outputFormat", default="rapidJSON"): Any("rapidJSON"),
21
+ Required("coordOutputFormat", default="WGS84"): Any("WGS84"),
22
+ Required("type_origin", default="any"): Any("any", "coord"),
23
+ Required("name_origin"): str,
24
+ Required("type_destination", default="any"): Any("any", "coord"),
25
+ Required("name_destination"): str,
26
+ Optional("type_via", default="any"): Any("any", "coord"),
27
+ Optional("name_via"): str,
28
+ Optional("useUT"): Any("0", "1", 0, 1),
29
+ Optional("useRealtime"): Any("0", "1", 0, 1),
30
+ }
31
+ )
File without changes
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Parser(ABC):
5
+ @abstractmethod
6
+ def parse(data) -> dict:
7
+ raise NotImplementedError
@@ -0,0 +1,11 @@
1
+ import json
2
+
3
+ from apyefa.commands.parsers.parser import Parser
4
+
5
+
6
+ class RapidJsonParser(Parser):
7
+ def parse(self, data: str) -> dict:
8
+ if not data:
9
+ return {}
10
+
11
+ return json.loads(data)
@@ -0,0 +1,6 @@
1
+ from apyefa.commands.parsers.parser import Parser
2
+
3
+
4
+ class XmlParser(Parser):
5
+ def parse(self, data: str) -> dict:
6
+ raise NotImplementedError