traewelling-api 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.
Files changed (40) hide show
  1. traewelling_api-0.1.0/LICENSE +8 -0
  2. traewelling_api-0.1.0/PKG-INFO +31 -0
  3. traewelling_api-0.1.0/README.md +12 -0
  4. traewelling_api-0.1.0/pyproject.toml +26 -0
  5. traewelling_api-0.1.0/setup.cfg +4 -0
  6. traewelling_api-0.1.0/src/traewelling_api/__init__.py +5 -0
  7. traewelling_api-0.1.0/src/traewelling_api/api/__init__.py +23 -0
  8. traewelling_api-0.1.0/src/traewelling_api/api/auth.py +52 -0
  9. traewelling_api-0.1.0/src/traewelling_api/api/checkin.py +226 -0
  10. traewelling_api-0.1.0/src/traewelling_api/api/community.py +0 -0
  11. traewelling_api-0.1.0/src/traewelling_api/api/configuration_information.py +16 -0
  12. traewelling_api-0.1.0/src/traewelling_api/api/dashboard.py +0 -0
  13. traewelling_api-0.1.0/src/traewelling_api/api/events.py +81 -0
  14. traewelling_api-0.1.0/src/traewelling_api/api/generic.py +100 -0
  15. traewelling_api-0.1.0/src/traewelling_api/api/ics_tokens.py +0 -0
  16. traewelling_api-0.1.0/src/traewelling_api/api/leaderboard.py +0 -0
  17. traewelling_api-0.1.0/src/traewelling_api/api/likes.py +0 -0
  18. traewelling_api-0.1.0/src/traewelling_api/api/model/__init__.py +2 -0
  19. traewelling_api-0.1.0/src/traewelling_api/api/model/request.py +107 -0
  20. traewelling_api-0.1.0/src/traewelling_api/api/model/response.py +71 -0
  21. traewelling_api-0.1.0/src/traewelling_api/api/notifications.py +0 -0
  22. traewelling_api-0.1.0/src/traewelling_api/api/polyline.py +0 -0
  23. traewelling_api-0.1.0/src/traewelling_api/api/report.py +0 -0
  24. traewelling_api-0.1.0/src/traewelling_api/api/security.py +0 -0
  25. traewelling_api-0.1.0/src/traewelling_api/api/settings.py +0 -0
  26. traewelling_api-0.1.0/src/traewelling_api/api/statistics.py +0 -0
  27. traewelling_api-0.1.0/src/traewelling_api/api/status.py +0 -0
  28. traewelling_api-0.1.0/src/traewelling_api/api/trips.py +0 -0
  29. traewelling_api-0.1.0/src/traewelling_api/api/user.py +0 -0
  30. traewelling_api-0.1.0/src/traewelling_api/api/user_follow.py +0 -0
  31. traewelling_api-0.1.0/src/traewelling_api/api/user_hide_and_block.py +0 -0
  32. traewelling_api-0.1.0/src/traewelling_api/api/webhooks.py +0 -0
  33. traewelling_api-0.1.0/src/traewelling_api/cli.py +120 -0
  34. traewelling_api-0.1.0/src/traewelling_api/client.py +125 -0
  35. traewelling_api-0.1.0/src/traewelling_api.egg-info/PKG-INFO +31 -0
  36. traewelling_api-0.1.0/src/traewelling_api.egg-info/SOURCES.txt +38 -0
  37. traewelling_api-0.1.0/src/traewelling_api.egg-info/dependency_links.txt +1 -0
  38. traewelling_api-0.1.0/src/traewelling_api.egg-info/entry_points.txt +2 -0
  39. traewelling_api-0.1.0/src/traewelling_api.egg-info/requires.txt +6 -0
  40. traewelling_api-0.1.0/src/traewelling_api.egg-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2026 Lambda Crime
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: traewelling-api
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://codeberg.org/lambda-crime/traewelling-api
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: fptf-pydantic>=0.1.0
13
+ Requires-Dist: pydantic-extra-types>=2.11.0
14
+ Requires-Dist: rich>=14.2.0
15
+ Requires-Dist: textual>=7.2.0
16
+ Requires-Dist: traewelling-pydantic>=0.2.1
17
+ Requires-Dist: typer>=0.21.1
18
+ Dynamic: license-file
19
+
20
+ # Traewelling-API
21
+
22
+ This is a simple Traewelling API wrapper.
23
+
24
+ The goal is to provide an easy way to talk with the Traewelling API from Python
25
+
26
+ ## TODOs
27
+
28
+ - add every API endpoint for Traewelling
29
+ - add FPTF converterter for Traewelling API responses
30
+ - add command for every API endpoint to CLI
31
+ - [OPTIONAL] add TUI for interactive API requests
@@ -0,0 +1,12 @@
1
+ # Traewelling-API
2
+
3
+ This is a simple Traewelling API wrapper.
4
+
5
+ The goal is to provide an easy way to talk with the Traewelling API from Python
6
+
7
+ ## TODOs
8
+
9
+ - add every API endpoint for Traewelling
10
+ - add FPTF converterter for Traewelling API responses
11
+ - add command for every API endpoint to CLI
12
+ - [OPTIONAL] add TUI for interactive API requests
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "traewelling-api"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "fptf-pydantic>=0.1.0",
9
+ "pydantic-extra-types>=2.11.0",
10
+ "rich>=14.2.0",
11
+ "textual>=7.2.0",
12
+ "traewelling-pydantic>=0.2.1",
13
+ "typer>=0.21.1",
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ license = "MIT"
20
+ license-files = ["LICENSE"]
21
+
22
+ [project.scripts]
23
+ traewelling-cli = "traewelling_api:app"
24
+
25
+ [project.urls]
26
+ Homepage = "https://codeberg.org/lambda-crime/traewelling-api"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from . import api
2
+ from . import client
3
+ from . import cli
4
+
5
+ from .cli import app
@@ -0,0 +1,23 @@
1
+ from . import auth
2
+ from . import checkin
3
+ from . import community
4
+ from . import configuration_information
5
+ from . import dashboard
6
+ from . import events
7
+ from . import ics_tokens
8
+ from . import leaderboard
9
+ from . import likes
10
+ from . import notifications
11
+ from . import polyline
12
+ from . import report
13
+ from . import security
14
+ from . import settings
15
+ from . import statistics
16
+ from . import status
17
+ from . import trips
18
+ from . import user
19
+ from . import user_follow
20
+ from . import user_hide_and_block
21
+ from . import webhooks
22
+ from . import generic
23
+ from . import models
@@ -0,0 +1,52 @@
1
+ """
2
+ Logging in, creating Accounts, etc.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from . import generic
8
+ import traewelling_pydantic.models as trwlm
9
+ from . import model
10
+
11
+
12
+ def post_auth_logout(token: str) -> bool:
13
+ """
14
+ Logout & invalidate current bearer token
15
+ """
16
+
17
+ generic.post(token=token, path="auth/logout")
18
+
19
+ return True
20
+
21
+
22
+ def get_auth_user(token: str) -> trwlm.UserAuth:
23
+ """
24
+ Get authenticated user information
25
+ """
26
+
27
+ response, status_code = generic.get(token=token, path="auth/user")
28
+
29
+ return trwlm.UserAuth.model_validate(response["data"])
30
+
31
+
32
+ def post_auth_refresh(token: str) -> model.response.BearerTokenResponse:
33
+ """
34
+ Refresh Bearer Token
35
+ """
36
+
37
+ response, status_code = generic.post(token=token, path="auth/refresh")
38
+
39
+ return model.response.BearerTokenResponse.model_validate(response["data"])
40
+
41
+
42
+ def get_user_statuses_active(token: str) -> Optional[trwlm.Status]:
43
+ """
44
+ User state
45
+ """
46
+
47
+ response, status_code = generic.get(token=token, path="user/statuses/active")
48
+
49
+ if status_code == 204:
50
+ return None
51
+
52
+ return trwlm.Status.model_validate(response["data"])
@@ -0,0 +1,226 @@
1
+ """
2
+ Checkin related endpoints. Regular process is departures -> trip -> checkin
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Optional, Union
7
+ import uuid
8
+ from pydantic_extra_types.coordinate import Latitude, Longitude
9
+
10
+ from . import generic
11
+ import traewelling_pydantic.api_models as trwla
12
+ import traewelling_pydantic.models as trwlm
13
+ from . import model
14
+ from requests.exceptions import HTTPError
15
+ from urllib.parse import quote
16
+
17
+
18
+ def get_operators(token: str) -> list[trwlm.Operator]:
19
+ response, status_code = generic.get(token=token, path="operators")
20
+
21
+ return [
22
+ trwlm.Operator.model_validate(operator) for operator in response["data"]
23
+ ]
24
+
25
+
26
+ def get_stations(
27
+ token: str,
28
+ identifier_provider: Optional[str] = None,
29
+ query: Optional[str] = None,
30
+ identifier: Optional[str] = None,
31
+ min_lat: Optional[Latitude] = None,
32
+ max_lat: Optional[Latitude] = None,
33
+ min_lon: Optional[Longitude] = None,
34
+ max_lon: Optional[Longitude] = None,
35
+ limit: int = 50,
36
+ with_identifiers: bool = False,
37
+ ) -> Optional[list[trwlm.Station]]:
38
+ """
39
+ UNSTABLE: Returns stations by fuzzy text, exact identifier, or within a bounding box (BBOX).
40
+ """
41
+
42
+ def valid_bounding_box(
43
+ min_lat: Optional[Latitude] = None,
44
+ max_lat: Optional[Latitude] = None,
45
+ min_lon: Optional[Longitude] = None,
46
+ max_lon: Optional[Longitude] = None,
47
+ ) -> bool:
48
+ if min_lat is None:
49
+ return False
50
+ if max_lat is None:
51
+ return False
52
+ if min_lon is None:
53
+ return False
54
+ if max_lon is None:
55
+ return False
56
+
57
+ if min_lat > max_lat:
58
+ return False
59
+
60
+ if min_lon > max_lon:
61
+ return False
62
+
63
+ return True
64
+
65
+ if (
66
+ query is not None
67
+ and (identifier is not None and identifier_provider is not None)
68
+ and valid_bounding_box(min_lat, max_lat, min_lon, max_lon)
69
+ ):
70
+ raise ValueError(
71
+ "Only specify one: query, identifier+identifier_provider or bounding box"
72
+ )
73
+
74
+ if query is not None:
75
+ params = {
76
+ "query": quote(query),
77
+ "limit": limit,
78
+ "withIdentifiers": with_identifiers,
79
+ }
80
+ elif identifier is not None:
81
+ params = {
82
+ "identifier_provider": identifier_provider,
83
+ "identifier": identifier,
84
+ "limit": limit,
85
+ "withIdentifiers": with_identifiers,
86
+ }
87
+ elif valid_bounding_box(min_lat, max_lat, min_lon, max_lon):
88
+ params = {
89
+ "min_lat": min_lat,
90
+ "max_lat": max_lat,
91
+ "min_lon": min_lon,
92
+ "max_lon": max_lon,
93
+ "limit": limit,
94
+ "withIdentifiers": with_identifiers,
95
+ }
96
+ else:
97
+ raise ValueError(
98
+ "Specify one: query, identifier+identifier_provider or bounding box"
99
+ )
100
+
101
+ try:
102
+ response, status_code = generic.get(token=token, path="stations", params=params)
103
+
104
+ return [trwlm.Station.model_validate(i) for i in response["data"]]
105
+ except HTTPError as error:
106
+ if error.response.status_code == 404:
107
+ return None
108
+ raise error
109
+
110
+
111
+ def get_stations_by_id(
112
+ token: str, station_id: Union[uuid.UUID, int], with_identifiers: bool = False
113
+ ) -> Optional[trwlm.Station]:
114
+ """
115
+ This request returns a single station object
116
+ """
117
+ try:
118
+ response, status_code = generic.get(
119
+ token=token,
120
+ path=f"station/{station_id}",
121
+ params={"withIdentifiers": with_identifiers},
122
+ )
123
+
124
+ return trwlm.Station.model_validate(response["data"])
125
+ except HTTPError as error:
126
+ if error.response.status_code == 404:
127
+ return None
128
+ raise error
129
+
130
+ def get_station_departures(token: str, station_id: Union[uuid.UUID, int], when: datetime, travelType: str) -> Optional[tuple[list[model.response.Departure], model.response.DepartureMeta]]:
131
+ """
132
+ Get departures from a station.
133
+ """
134
+ try:
135
+ response, status_code = generic.get(
136
+ token=token,
137
+ path=f"station/{station_id}/departures",
138
+ params={"when": when, "travelType": travelType},
139
+ )
140
+
141
+ return [model.response.Departure.model_validate(i) for i in response["data"]], model.response.DepartureMeta.model_validate(response["meta"])
142
+ except HTTPError as error:
143
+ if error.response.status_code == 404:
144
+ return None
145
+ raise error
146
+
147
+
148
+ def get_trains_trip(token: str, hafasTripId: str, lineName: str) -> Optional[trwlm.Trip]:
149
+ """
150
+ Get the stopovers and trip information for a given train
151
+ """
152
+ try:
153
+ response, status_code = generic.get(
154
+ token=token,
155
+ path="trains/trip",
156
+ params={"hafasTripId": hafasTripId, "lineName": lineName},
157
+ )
158
+ return trwlm.Trip.model_validate(response["data"])
159
+ except HTTPError as error:
160
+ if error.response.status_code == 404:
161
+ return None
162
+ raise error
163
+
164
+ def get_trains_station_nearby(token: str, latitude: Latitude, longitude: Longitude) -> Optional[list[trwlm.Station]]:
165
+ """
166
+ Location based search for stations
167
+ """
168
+ try:
169
+ response, status_code = generic.get(
170
+ token=token,
171
+ path="trains/trip",
172
+ params={"latidtude": latitude, "longitude": longitude},
173
+ )
174
+ return [trwlm.Station.model_validate(i) for i in response["data"]]
175
+ except HTTPError as error:
176
+ if error.response.status_code == 404:
177
+ return None
178
+ raise error
179
+
180
+ def post_trains_checkin(token: str, checkin: model.request.CheckinRequest) -> trwla.CheckinResponse:
181
+ """
182
+ Check in to a trip.
183
+ """
184
+ response, status_code = generic.post(
185
+ token=token,
186
+ path="trains/checkin",
187
+ data=checkin.model_dump(mode="json")
188
+ )
189
+
190
+ return trwla.CheckinResponse.model_validate(response)
191
+
192
+ def put_station_as_home(token: str, station_id: int) -> Optional[trwlm.Station]:
193
+ """
194
+ Set a station as home station
195
+ """
196
+ try:
197
+ response, status_code = generic.put(
198
+ token=token,
199
+ path=f"station/{station_id}/home",
200
+ )
201
+ return trwlm.Station.model_validate(response["data"])
202
+ except HTTPError as error:
203
+ if error.response.status_code == 404:
204
+ return None
205
+ raise error
206
+
207
+ def get_trains_station_autocomplete(token: str, query: str) -> list[trwlm.Station]:
208
+ """
209
+ This request returns an array of max. 10 station objects matching the query.
210
+ """
211
+ response, status_code = generic.get(
212
+ token=token,
213
+ path=f"trains/station/autocomplete/{quote(query)}",
214
+ )
215
+ return [trwlm.Station.model_validate(i) for i in response["data"]]
216
+
217
+
218
+ def get_trains_station_history(token: str) -> list[trwlm.Station]:
219
+ """
220
+ This request returns an array of max. 10 most recent station objects that the user has arrived at.
221
+ """
222
+ response, status_code = generic.get(
223
+ token=token,
224
+ path="trains/station/history",
225
+ )
226
+ return [trwlm.Station.model_validate(i) for i in response["data"]]
@@ -0,0 +1,16 @@
1
+ """
2
+ Endpoints related to application configuration information.
3
+ """
4
+
5
+ from . import generic
6
+ import traewelling_pydantic.config as trwlc
7
+
8
+
9
+ def get_app_configuration(token: str) -> trwlc.ConfigurationInformation:
10
+ """
11
+ Retrieves configuration information about the application, including features and supported languages.
12
+ """
13
+
14
+ response, status_code = generic.get(token=token, path="app/configuration")
15
+
16
+ return trwlc.ConfigurationInformation.model_validate(response)
@@ -0,0 +1,81 @@
1
+ """
2
+ Events that users can check in to
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+ import traewelling_pydantic.models as trwlm
9
+ from requests import HTTPError
10
+
11
+ from . import generic, model
12
+
13
+
14
+ def get_event(token: str, slug: str) -> trwlm.Event:
15
+ """
16
+ [Auth optional] Get basic information for event
17
+ """
18
+ response, status_code = generic.get(token=token, path=f"event/{slug}")
19
+
20
+ return trwlm.Event.model_validate(response["data"])
21
+
22
+
23
+ def get_events(
24
+ token: str, timestamp: datetime, only_upcoming: bool = False
25
+ ) -> list[trwlm.Event]:
26
+ """
27
+ Returns all active or upcoming events for the given timestamp. Default timestamp is now. If upcoming is set to true, all events ending after the timestamp are returned.
28
+ """
29
+ response, status_code = generic.get(
30
+ token=token,
31
+ path="events",
32
+ params={"timestamp": timestamp.isoformat(), "upcoming": only_upcoming},
33
+ )
34
+
35
+ return [trwlm.Event.model_validate(event) for event in response["data"]]
36
+
37
+
38
+ def get_event_details(token: str, slug: str) -> model.response.EventDetailsResponse:
39
+ """
40
+ Returns overall travelled distance and duration for an event
41
+ """
42
+ response, status_code = generic.get(token=token, path=f"event/{slug}/details")
43
+
44
+ return model.response.EventDetailsResponse.model_validate(response["data"])
45
+
46
+
47
+ def get_event_statuses(
48
+ token: str, slug: str, page: int
49
+ ) -> Optional[tuple[list[trwlm.Event], model.response.Links, model.response.Meta]]:
50
+ """
51
+ Returns all for user visible statuses for an event
52
+ """
53
+ try:
54
+ response, status_code = generic.get(
55
+ token=token,
56
+ path=f"event/{slug}/statuses",
57
+ params={"page": page},
58
+ )
59
+ except HTTPError as error:
60
+ if error.response.status_code == 404:
61
+ return None
62
+ raise error
63
+
64
+ return (
65
+ [trwlm.Event.model_validate(event) for event in response["data"]],
66
+ model.response.Links.model_validate(response["links"]),
67
+ model.response.Meta.model_validate(response["meta"]),
68
+ )
69
+
70
+
71
+ def post_event(
72
+ token: str, event_suggestion: model.request.EventSuggestionRequest
73
+ ) -> bool:
74
+ """
75
+ Submit a possible event for our administrators to publish
76
+ """
77
+ response, status_code = generic.post(
78
+ token=token, path="event", data=event_suggestion.model_dump(mode="json")
79
+ )
80
+
81
+ return True
@@ -0,0 +1,100 @@
1
+ from typing import Optional
2
+ import requests
3
+ from urllib.parse import urljoin
4
+
5
+
6
+ def request(
7
+ token: str,
8
+ path: str,
9
+ method: str = "GET",
10
+ params: Optional[dict] = None,
11
+ data: Optional[dict] = None,
12
+ headers: Optional[dict] = None,
13
+ base_url: str = "https://traewelling.de/api/v1/",
14
+ ) -> tuple[dict, int]:
15
+ response = requests.request(
16
+ method=method.upper(),
17
+ url=urljoin(base_url, path),
18
+ params=params,
19
+ headers={"Authorization": f"Bearer {token}"}
20
+ if headers is None
21
+ else {"Authorization": f"Bearer {token}", **headers},
22
+ data=data,
23
+ )
24
+
25
+ response.raise_for_status()
26
+
27
+ return response.json(), response.status_code
28
+
29
+
30
+ def get(
31
+ token: str,
32
+ path: str,
33
+ params: Optional[dict] = None,
34
+ data: Optional[dict] = None,
35
+ headers: Optional[dict] = None,
36
+ base_url: str = "https://traewelling.de/api/v1/",
37
+ ) -> tuple[dict, int]:
38
+ return request(
39
+ method="GET",
40
+ token=token,
41
+ path=path,
42
+ params=params,
43
+ data=data,
44
+ headers=headers,
45
+ base_url=base_url,
46
+ )
47
+
48
+ def put(
49
+ token: str,
50
+ path: str,
51
+ params: Optional[dict] = None,
52
+ data: Optional[dict] = None,
53
+ headers: Optional[dict] = None,
54
+ base_url: str = "https://traewelling.de/api/v1/",
55
+ ) -> tuple[dict, int]:
56
+ return request(
57
+ method="PUT",
58
+ token=token,
59
+ path=path,
60
+ params=params,
61
+ data=data,
62
+ headers=headers,
63
+ base_url=base_url,
64
+ )
65
+
66
+ def post(
67
+ token: str,
68
+ path: str,
69
+ params: Optional[dict] = None,
70
+ data: Optional[dict] = None,
71
+ headers: Optional[dict] = None,
72
+ base_url: str = "https://traewelling.de/api/v1/",
73
+ ) -> tuple[dict, int]:
74
+ return request(
75
+ method="POST",
76
+ token=token,
77
+ path=path,
78
+ params=params,
79
+ data=data,
80
+ headers=headers,
81
+ base_url=base_url,
82
+ )
83
+
84
+ def delete(
85
+ token: str,
86
+ path: str,
87
+ params: Optional[dict] = None,
88
+ data: Optional[dict] = None,
89
+ headers: Optional[dict] = None,
90
+ base_url: str = "https://traewelling.de/api/v1/",
91
+ ) -> tuple[dict, int]:
92
+ return request(
93
+ method="DELETE",
94
+ token=token,
95
+ path=path,
96
+ params=params,
97
+ data=data,
98
+ headers=headers,
99
+ base_url=base_url,
100
+ )
File without changes
@@ -0,0 +1,2 @@
1
+ from . import request
2
+ from . import response
@@ -0,0 +1,107 @@
1
+ import traewelling_pydantic as trwl
2
+ from typing import Optional, Union
3
+ import uuid
4
+ import pydantic
5
+
6
+ from datetime import datetime
7
+
8
+
9
+ class TraewellingRequestModel(trwl.models.TraewellingModel):
10
+ pass
11
+
12
+
13
+ class UpdateProfileInformationRequest(TraewellingRequestModel):
14
+ username: str
15
+ displayName: str
16
+ privateProfile: Optional[bool] = None
17
+ preventIndex: Optional[bool] = None
18
+ privacyHideDays: Optional[int] = None
19
+ defaultStatusVisibility: Optional[trwl.enums.Visibility] = None
20
+ mastodonVisibility: Optional[trwl.enums.MastodonVisibility] = None
21
+ mapProvider: Optional[trwl.enums.MapProvider] = None
22
+ friendCheckin: Optional[trwl.enums.FriendCheckinSetting] = None
23
+ pointsEnabled: Optional[bool] = None
24
+ bio: Optional[str] = None
25
+ experimental: bool
26
+ profileLinks: Optional[list[trwl.models.ProfileLink]] = None
27
+ timezone: str
28
+
29
+
30
+ class CheckinRequest(TraewellingRequestModel):
31
+ body: str
32
+ business: trwl.enums.Business
33
+ visibility: trwl.enums.Visibility
34
+ eventId: int
35
+ toot: bool
36
+ chainPost: bool
37
+ ibnr: bool
38
+ tripId: str
39
+ lineName: str
40
+ start: int
41
+ destination: int
42
+ departure: datetime
43
+ arrival: datetime
44
+ force: bool
45
+ with_: list[Union[int, uuid.UUID]] = pydantic.Field(alias="with")
46
+
47
+
48
+ class EventSuggestionRequest(TraewellingRequestModel):
49
+ name: str
50
+ host: str
51
+ begin: datetime
52
+ end: datetime
53
+ url: Optional[str] = None
54
+ hashtag: Optional[str] = None
55
+ nearestStationId: Optional[int] = None
56
+
57
+
58
+ class ICSTokenRequest(TraewellingRequestModel):
59
+ name: str
60
+
61
+
62
+ class ICSTokenDeleteRequest(TraewellingRequestModel):
63
+ tokenId: int
64
+
65
+
66
+ class PolylineRequest(TraewellingRequestModel):
67
+ from_station_id: int
68
+ to_station_id: int
69
+ stopover_id: int
70
+
71
+
72
+ class ReportRequest(TraewellingRequestModel):
73
+ subjectType: str
74
+ subjectId: int
75
+ reason: str
76
+ description: str
77
+
78
+
79
+ class UpdateEmailRequest(TraewellingRequestModel):
80
+ email: str
81
+ password: str
82
+
83
+
84
+ class AccountDeletionRequest(TraewellingRequestModel):
85
+ confirmation: str
86
+
87
+
88
+ class UpdateStatusRequest(TraewellingRequestModel):
89
+ body: Optional[str] = None
90
+ business: Optional[trwl.enums.Business] = None
91
+ visibility: Optional[trwl.enums.Visibility] = None
92
+ eventId: Optional[str] = None
93
+ manualDeparture: Optional[datetime] = None
94
+ manualArrival: Optional[datetime] = None
95
+ destinationId: Optional[str] = None
96
+ destinationArrivalPlanned: Optional[datetime] = None
97
+
98
+
99
+ class UpdateStatusTagRequest(TraewellingRequestModel):
100
+ key: str
101
+ value: str
102
+ visibility: trwl.enums.Visibility
103
+
104
+
105
+ class UpdateUserTrustedRequest(TraewellingRequestModel):
106
+ userId: str
107
+ expiresAt: datetime
@@ -0,0 +1,71 @@
1
+ from datetime import datetime
2
+ from typing import Union, Optional
3
+ import uuid
4
+ import pydantic
5
+ import traewelling_pydantic as trwl
6
+ import fptf_pydantic.models as fptf
7
+
8
+
9
+ class TraewellingResponseModel(trwl.models.TraewellingModel):
10
+ pass
11
+
12
+
13
+ class Meta(TraewellingResponseModel):
14
+ current_page: int
15
+ from_: int = pydantic.Field(alias="from")
16
+ path: str
17
+ per_page: int
18
+ to: int
19
+
20
+
21
+ class Links(TraewellingResponseModel):
22
+ first: str
23
+ last: Optional[str] = None
24
+ prev: Optional[str] = None
25
+ next_: Optional[str] = pydantic.Field(default=None, alias="next")
26
+
27
+
28
+ class BearerTokenResponse(TraewellingResponseModel):
29
+ token: str
30
+ expires_at: datetime
31
+
32
+
33
+ class LikeResponse(TraewellingResponseModel):
34
+ count: int
35
+
36
+
37
+ class CheckinResponse(TraewellingResponseModel):
38
+ status: trwl.models.Status
39
+ points: trwl.models.Points
40
+ alsoOnThisConnection: list[trwl.models.Status]
41
+
42
+
43
+ class EventDetailsResponse(TraewellingResponseModel):
44
+ id: int
45
+ slug: str
46
+ totalDistance: int
47
+ totalDuration: int
48
+
49
+ class Departure(trwl.models.TraewellingModel):
50
+ tripId: str
51
+ stop: fptf.Stop
52
+ when: datetime
53
+ plannedWhen: datetime
54
+ platform: str
55
+ plannedPlatform: str
56
+ direction: str
57
+ line: fptf.Line
58
+ station: trwl.models.Station
59
+
60
+
61
+ class DepartureMetaTimes(TraewellingResponseModel):
62
+ now: datetime
63
+ prev: datetime
64
+ next_: datetime = pydantic.Field(alias="next")
65
+
66
+
67
+ class DepartureMeta(TraewellingResponseModel):
68
+ station: trwl.models.Station
69
+ times: DepartureMetaTimes
70
+ removedLicenses: list[Union[str, trwl.models.LicenseDTO]]
71
+ removedCount: int
File without changes
File without changes
@@ -0,0 +1,120 @@
1
+ import json
2
+ from typing import Annotated, Optional
3
+
4
+ from pydantic_extra_types.coordinate import Latitude, Longitude
5
+
6
+ from . import client
7
+ import traewelling_pydantic.models as traewelling
8
+ import typer
9
+ from rich.console import Console
10
+ from requests.exceptions import HTTPError
11
+ from requests import status_codes
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+
17
+ @app.command("station", no_args_is_help=True)
18
+ def get_station(
19
+ token: Annotated[str, typer.Option("--token", "-t", envvar="TRAEWELLING_TOKEN")],
20
+ station_id: Annotated[Optional[int], typer.Option("--station-id", "-i")] = None,
21
+ name: Annotated[Optional[str], typer.Option("--name", "-n")] = None,
22
+ identifier: Annotated[Optional[str], typer.Option("--identifier")] = None,
23
+ identifier_provider: Annotated[
24
+ Optional[str], typer.Option("--identifier-provider")
25
+ ] = None,
26
+ bounding_box: Annotated[
27
+ Optional[tuple[float, float, float, float]], typer.Option("--bounding-box")
28
+ ] = None,
29
+ with_identifiers: Annotated[bool, typer.Option("--with-identifiers")] = False,
30
+ as_json: Annotated[bool, typer.Option("--json", "-j", envvar="AS_JSON")] = False,
31
+ ):
32
+ trwl = client.TraewellingClient(token)
33
+
34
+ try:
35
+ response = trwl.get_stations(
36
+ query=name,
37
+ identifier=identifier,
38
+ identifier_provider=identifier_provider,
39
+ min_lat=Latitude(bounding_box[0]) if bounding_box is not None else None,
40
+ max_lat=Latitude(bounding_box[1]) if bounding_box is not None else None,
41
+ min_lon=Longitude(bounding_box[2]) if bounding_box is not None else None,
42
+ max_lon=Longitude(bounding_box[3]) if bounding_box is not None else None,
43
+ with_identifiers=with_identifiers
44
+ )
45
+
46
+ except ValueError:
47
+ if station_id is None:
48
+ raise ValueError("No valid identifiers or bounding box specified")
49
+
50
+ response = trwl.get_station_by_id(station_id, with_identifiers)
51
+
52
+
53
+ if as_json:
54
+ if response is not None:
55
+ if isinstance(response, list):
56
+ response = json.dumps([i.model_dump(mode="json") for i in response])
57
+ else:
58
+ response = response.model_dump_json()
59
+
60
+ console.print_json(response)
61
+ else:
62
+ console.print_json(None)
63
+ else:
64
+ console.print(response)
65
+
66
+
67
+ @app.command("stations")
68
+ def get_stations(
69
+ token: Annotated[str, typer.Option("--token", "-t", envvar="TRAEWELLING_TOKEN")],
70
+ ids: list[int],
71
+ with_identifiers: Annotated[bool, typer.Option("--with-identifiers")] = False,
72
+ as_json: Annotated[bool, typer.Option("--json", "-j", envvar="AS_JSON")] = False,
73
+ ):
74
+ trwl = client.TraewellingClient(token)
75
+ responses: list[Optional[traewelling.Station]] = []
76
+
77
+ for id in ids:
78
+ try:
79
+ responses.append(trwl.get_station_by_id(id, with_identifiers))
80
+ except HTTPError as error:
81
+ if not as_json:
82
+ if (
83
+ error.response.status_code == status_codes.codes.forbidden
84
+ or error.response.status_code == status_codes.codes.unauthorized
85
+ ):
86
+ console.print(
87
+ f"Don't have permission to query the station for ID {id}"
88
+ )
89
+ elif error.response.status_code == status_codes.codes.too_many_requests:
90
+ console.print("Too many requests")
91
+
92
+ if as_json:
93
+ console.print_json(
94
+ json.dumps(
95
+ [
96
+ response.model_dump() if response is not None else None
97
+ for response in responses
98
+ ]
99
+ )
100
+ )
101
+ else:
102
+ console.print(responses)
103
+
104
+
105
+ @app.command("operators")
106
+ def get_operators(
107
+ token: Annotated[str, typer.Option("--token", "-t", envvar="TRAEWELLING_TOKEN")],
108
+ as_json: Annotated[bool, typer.Option("--json", "-j", envvar="AS_JSON")] = False,
109
+ ):
110
+ trwl = client.TraewellingClient(token)
111
+ response = trwl.get_operators()
112
+
113
+ if as_json:
114
+ console.print_json(json.dumps([r.model_dump() for r in response]))
115
+ else:
116
+ console.print(response)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ app()
@@ -0,0 +1,125 @@
1
+ import uuid
2
+ from datetime import datetime
3
+ from typing import Optional, Union
4
+
5
+ import traewelling_pydantic.models as traewelling
6
+ from requests.exceptions import HTTPError
7
+ from requests.status_codes import codes as httpCode
8
+ from pydantic_extra_types.coordinate import Latitude, Longitude
9
+
10
+ from . import api
11
+
12
+
13
+ class TraewellingClient:
14
+ """
15
+ Traewelling Client object to manage the token
16
+
17
+ Behavior Note: 404s will return None
18
+ """
19
+
20
+ def __init__(self, token: str):
21
+ self.token = token
22
+
23
+ def get_stations(
24
+ self,
25
+ identifier_provider: Optional[str] = None,
26
+ query: Optional[str] = None,
27
+ identifier: Optional[str] = None,
28
+ min_lat: Optional[Latitude] = None,
29
+ max_lat: Optional[Latitude] = None,
30
+ min_lon: Optional[Longitude] = None,
31
+ max_lon: Optional[Longitude] = None,
32
+ limit: int = 50,
33
+ with_identifiers: bool = False,
34
+ ) -> Optional[list[traewelling.Station]]:
35
+ try:
36
+ return api.checkin.get_stations(
37
+ self.token,
38
+ identifier=identifier,
39
+ identifier_provider=identifier_provider,
40
+ min_lat=min_lat,
41
+ max_lat=max_lat,
42
+ min_lon=min_lon,
43
+ max_lon=max_lon,
44
+ limit=limit,
45
+ with_identifiers=with_identifiers,
46
+ query=query
47
+ )
48
+ except HTTPError as error:
49
+ if error.response.status_code == httpCode.not_found:
50
+ return None
51
+ raise error
52
+
53
+ def get_station_by_id(
54
+ self,
55
+ station_id: Union[int, uuid.UUID],
56
+ with_identifiers: bool = False,
57
+ ) -> Optional[traewelling.Station]:
58
+ """
59
+ This request returns a single station object
60
+
61
+ :param station_id: Either the traewelling integer ID or UUID
62
+ :type station_id: int, uuid.UUID
63
+
64
+ :param with_identifiers: If further identifiers should be requested from Traewelling
65
+ :type with_identifiers: bool
66
+
67
+ :raises HTTPError: Might raise a 401, 403, or 5xx error
68
+
69
+ :return: Either returns a Station object or in case of a 404 returns None
70
+ :rtype: Station(optional)
71
+ """
72
+ try:
73
+ return api.checkin.get_stations_by_id(
74
+ self.token, station_id=station_id, with_identifiers=with_identifiers
75
+ )
76
+ except HTTPError as error:
77
+ if error.response.status_code == httpCode.not_found:
78
+ return None
79
+ raise error
80
+
81
+ def get_operators(self) -> list[traewelling.Operator]:
82
+ """
83
+ Get a list of 'all' operators.
84
+
85
+ :raises HTTPError: Might raise a 401, 403, or 5xx error
86
+
87
+ :return: Returns a list of Operator objects
88
+ :rtype: list[Operator]
89
+ """
90
+
91
+ return api.checkin.get_operators(self.token)
92
+
93
+ def get_event(self, slug: str) -> Optional[traewelling.Event]:
94
+ """
95
+ Get basic information for event
96
+
97
+ :param slug: A slug for an event
98
+ :type slug: str
99
+
100
+ :raises HTTPError: Might raise a 401, 403, or 5xx error
101
+
102
+ :return: Either returns an Event object or in case of a 404 returns None
103
+ :rtype: Event(optional)
104
+ """
105
+ try:
106
+ return api.events.get_event(self.token, slug=slug)
107
+ except HTTPError as error:
108
+ if error.response.status_code == httpCode.not_found:
109
+ return None
110
+ raise error
111
+
112
+ def get_events(
113
+ self, timestamp: datetime, only_upcoming: bool = False
114
+ ) -> list[traewelling.Event]:
115
+ """
116
+ Returns all active or upcoming events for the given timestamp. Default timestamp is now. If upcoming is set to true, all events ending after the timestamp are returned.
117
+
118
+ :raises HTTPError: Might raise a 401, 403, or 5xx error
119
+
120
+ :return: Returns a list of Event objects
121
+ :rtype: list[Event]
122
+ """
123
+ return api.events.get_events(
124
+ self.token, timestamp=timestamp, only_upcoming=only_upcoming
125
+ )
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: traewelling-api
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://codeberg.org/lambda-crime/traewelling-api
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: fptf-pydantic>=0.1.0
13
+ Requires-Dist: pydantic-extra-types>=2.11.0
14
+ Requires-Dist: rich>=14.2.0
15
+ Requires-Dist: textual>=7.2.0
16
+ Requires-Dist: traewelling-pydantic>=0.2.1
17
+ Requires-Dist: typer>=0.21.1
18
+ Dynamic: license-file
19
+
20
+ # Traewelling-API
21
+
22
+ This is a simple Traewelling API wrapper.
23
+
24
+ The goal is to provide an easy way to talk with the Traewelling API from Python
25
+
26
+ ## TODOs
27
+
28
+ - add every API endpoint for Traewelling
29
+ - add FPTF converterter for Traewelling API responses
30
+ - add command for every API endpoint to CLI
31
+ - [OPTIONAL] add TUI for interactive API requests
@@ -0,0 +1,38 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/traewelling_api/__init__.py
5
+ src/traewelling_api/cli.py
6
+ src/traewelling_api/client.py
7
+ src/traewelling_api.egg-info/PKG-INFO
8
+ src/traewelling_api.egg-info/SOURCES.txt
9
+ src/traewelling_api.egg-info/dependency_links.txt
10
+ src/traewelling_api.egg-info/entry_points.txt
11
+ src/traewelling_api.egg-info/requires.txt
12
+ src/traewelling_api.egg-info/top_level.txt
13
+ src/traewelling_api/api/__init__.py
14
+ src/traewelling_api/api/auth.py
15
+ src/traewelling_api/api/checkin.py
16
+ src/traewelling_api/api/community.py
17
+ src/traewelling_api/api/configuration_information.py
18
+ src/traewelling_api/api/dashboard.py
19
+ src/traewelling_api/api/events.py
20
+ src/traewelling_api/api/generic.py
21
+ src/traewelling_api/api/ics_tokens.py
22
+ src/traewelling_api/api/leaderboard.py
23
+ src/traewelling_api/api/likes.py
24
+ src/traewelling_api/api/notifications.py
25
+ src/traewelling_api/api/polyline.py
26
+ src/traewelling_api/api/report.py
27
+ src/traewelling_api/api/security.py
28
+ src/traewelling_api/api/settings.py
29
+ src/traewelling_api/api/statistics.py
30
+ src/traewelling_api/api/status.py
31
+ src/traewelling_api/api/trips.py
32
+ src/traewelling_api/api/user.py
33
+ src/traewelling_api/api/user_follow.py
34
+ src/traewelling_api/api/user_hide_and_block.py
35
+ src/traewelling_api/api/webhooks.py
36
+ src/traewelling_api/api/model/__init__.py
37
+ src/traewelling_api/api/model/request.py
38
+ src/traewelling_api/api/model/response.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ traewelling-cli = traewelling_api:app
@@ -0,0 +1,6 @@
1
+ fptf-pydantic>=0.1.0
2
+ pydantic-extra-types>=2.11.0
3
+ rich>=14.2.0
4
+ textual>=7.2.0
5
+ traewelling-pydantic>=0.2.1
6
+ typer>=0.21.1
@@ -0,0 +1 @@
1
+ traewelling_api