python-terminusgps 1.9.8__tar.gz → 1.10.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.

Potentially problematic release.


This version of python-terminusgps might be problematic. Click here for more details.

Files changed (36) hide show
  1. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/PKG-INFO +2 -1
  2. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/pyproject.toml +2 -1
  3. python_terminusgps-1.10.0/terminusgps/settings.py +12 -0
  4. python_terminusgps-1.10.0/terminusgps/twilio/caller.py +65 -0
  5. python_terminusgps-1.10.0/terminusgps/wialon/__init__.py +0 -0
  6. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/base.py +10 -11
  7. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/unit.py +48 -6
  8. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/session.py +1 -4
  9. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/utils.py +1 -0
  10. python_terminusgps-1.10.0/uv.lock +835 -0
  11. python_terminusgps-1.9.8/terminusgps/settings.py +0 -8
  12. python_terminusgps-1.9.8/uv.lock +0 -553
  13. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/.gitignore +0 -0
  14. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/COPYING +0 -0
  15. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/README.md +0 -0
  16. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/requirements.txt +0 -0
  17. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/__init__.py +0 -0
  18. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/__init__.py +0 -0
  19. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/auth.py +0 -0
  20. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/profiles/__init__.py +0 -0
  21. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/profiles/addresses.py +0 -0
  22. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/profiles/base.py +0 -0
  23. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/profiles/customers.py +0 -0
  24. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/authorizenet/profiles/payments.py +0 -0
  25. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/aws/__init__.py +0 -0
  26. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/aws/secrets.py +0 -0
  27. {python_terminusgps-1.9.8/terminusgps/wialon → python_terminusgps-1.10.0/terminusgps/twilio}/__init__.py +0 -0
  28. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/constants.py +0 -0
  29. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/errors.py +0 -0
  30. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/flags.py +0 -0
  31. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/__init__.py +0 -0
  32. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/resource.py +0 -0
  33. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/retranslator.py +0 -0
  34. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/route.py +0 -0
  35. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/unit_group.py +0 -0
  36. {python_terminusgps-1.9.8 → python_terminusgps-1.10.0}/terminusgps/wialon/items/user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-terminusgps
3
- Version: 1.9.8
3
+ Version: 1.10.0
4
4
  Summary: Provides abstractions/utilities for working with Wialon API, Authorize.NET API, AWS API, and more.
5
5
  Project-URL: Documentation, https://app.terminusgps.com/docs/apps/python-terminusgps/index.html
6
6
  Project-URL: Repository, https://github.com/terminusgps/python-terminusgps
@@ -17,3 +17,4 @@ Requires-Dist: authorizenet>=1.1.5
17
17
  Requires-Dist: boto3>=1.34.144
18
18
  Requires-Dist: django>=5.1.5
19
19
  Requires-Dist: python-wialon>=1.2.4
20
+ Requires-Dist: twilio>=9.4.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-terminusgps"
3
- version = "1.9.8"
3
+ version = "1.10.0"
4
4
  description = "Provides abstractions/utilities for working with Wialon API, Authorize.NET API, AWS API, and more."
5
5
  readme = "README.md"
6
6
  authors = [ {name = "Blake Nall", email = "blake@terminusgps.com" } ]
@@ -18,6 +18,7 @@ dependencies = [
18
18
  "boto3>=1.34.144",
19
19
  "django>=5.1.5",
20
20
  "python-wialon>=1.2.4",
21
+ "twilio>=9.4.5",
21
22
  ]
22
23
 
23
24
  [project.urls]
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+ DEBUG = True
4
+ MERCHANT_AUTH_LOGIN_ID = os.getenv("MERCHANT_AUTH_LOGIN_ID")
5
+ MERCHANT_AUTH_TRANSACTION_KEY = os.getenv("MERCHANT_AUTH_TRANSACTION_KEY")
6
+ SECRET_KEY = "xem*6no%8d9%^qzt2f3x3ar-uq4_+7h9myc$t0!+4%bj5us6f)"
7
+ TWILIO_FROM_NUMBER = os.getenv("TWILIO_FROM_NUMBER")
8
+ TWILIO_MESSAGING_SID = os.getenv("TWILIO_MESSAGING_SID")
9
+ TWILIO_SID = os.getenv("TWILIO_SID")
10
+ TWILIO_TOKEN = os.getenv("TWILIO_TOKEN")
11
+ WIALON_ADMIN_ID = os.getenv("WIALON_ADMIN_ID")
12
+ WIALON_TOKEN = os.getenv("WIALON_TOKEN")
@@ -0,0 +1,65 @@
1
+ from typing import Any
2
+
3
+ import asyncio
4
+ import twilio.rest
5
+
6
+ from django.conf import settings, ImproperlyConfigured
7
+ from twilio.http.async_http_client import AsyncTwilioHttpClient
8
+
9
+ if not hasattr(settings, "TWILIO_FROM_NUMBER"):
10
+ raise ImproperlyConfigured("'TWILIO_FROM_NUMBER' setting is required.")
11
+ if not hasattr(settings, "TWILIO_MESSAGING_SID"):
12
+ raise ImproperlyConfigured("'TWILIO_MESSAGING_SID' setting is required.")
13
+ if not hasattr(settings, "TWILIO_SID"):
14
+ raise ImproperlyConfigured("'TWILIO_SID' setting is required.")
15
+ if not hasattr(settings, "TWILIO_TOKEN"):
16
+ raise ImproperlyConfigured("'TWILIO_TOKEN' setting is required.")
17
+
18
+
19
+ class TwilioCaller:
20
+ def __init__(self) -> None:
21
+ self.from_number = settings.TWILIO_FROM_NUMBER
22
+ self.messaging_service_sid = settings.TWILIO_MESSAGING_SID
23
+
24
+ def __enter__(self) -> "TwilioCaller":
25
+ self.client = twilio.rest.Client(
26
+ settings.TWILIO_SID,
27
+ settings.TWILIO_TOKEN,
28
+ http_client=AsyncTwilioHttpClient(),
29
+ )
30
+ return self
31
+
32
+ def __exit__(self, exc_type, exc_value, exc_tb) -> None:
33
+ return None
34
+
35
+ async def create_notification(
36
+ self, to_number: str, message: str, method: str = "sms"
37
+ ) -> asyncio.Task[Any]:
38
+ match method:
39
+ case "sms":
40
+ task = asyncio.create_task(
41
+ self.create_sms(to_number=to_number, message=message)
42
+ )
43
+ case "call" | "phone":
44
+ task = asyncio.create_task(
45
+ self.create_call(to_number=to_number, message=message)
46
+ )
47
+ case _:
48
+ raise ValueError(f"Unsupported TwilioCaller method '{method}'.")
49
+
50
+ return task
51
+
52
+ async def create_call(self, to_number: str, message: str) -> None:
53
+ await self.client.calls.create_async(
54
+ to=to_number,
55
+ from_=self.from_number,
56
+ twiml=f"<Response><Say>{message}</Say></Response>",
57
+ )
58
+
59
+ async def create_sms(self, to_number: str, message: str) -> None:
60
+ await self.client.messages.create_async(
61
+ to=to_number,
62
+ from_=self.from_number,
63
+ body=message,
64
+ messaging_service_sid=self.messaging_service_sid,
65
+ )
@@ -1,17 +1,15 @@
1
- import terminusgps.wialon.flags as flags
1
+ from abc import abstractmethod
2
+ from terminusgps.wialon import flags
2
3
  from terminusgps.wialon.session import WialonSession
3
4
 
4
5
 
5
6
  class WialonBase:
6
- def __init__(
7
- self, *, id: str | None = None, session: WialonSession, **kwargs
8
- ) -> None:
9
- self._session = session
7
+ def __init__(self, id: str | int, session: WialonSession, **kwargs) -> None:
8
+ if isinstance(id, str) and not id.isdigit():
9
+ raise ValueError(f"'id' must be a digit. Got '{id}'.")
10
10
 
11
- if not id:
12
- self._id = self.create(**kwargs)
13
- else:
14
- self._id = id
11
+ self._session = session
12
+ self._id = str(id if id else self.create(**kwargs))
15
13
  self.populate()
16
14
 
17
15
  def __str__(self) -> str:
@@ -20,7 +18,7 @@ class WialonBase:
20
18
  def populate(self) -> None:
21
19
  response = self.session.wialon_api.core_search_item(
22
20
  **{"id": str(self.id), "flags": 0x1}
23
- )["item"]
21
+ ).get("item", {})
24
22
  self.name = response.get("nm")
25
23
  self.hw_type = response.get("cls")
26
24
  self.access_lvl = response.get("uacl")
@@ -59,8 +57,9 @@ class WialonBase:
59
57
  )
60
58
  return True if self.id in response.keys() else False
61
59
 
60
+ @abstractmethod
62
61
  def create(self) -> int | None:
63
- """Creates a Wialon object and returns the newly created Wialon object's id."""
62
+ """Creates a Wialon object and returns its id."""
64
63
  raise NotImplementedError("Subclasses must implement this method.")
65
64
 
66
65
  def rename(self, new_name: str) -> None:
@@ -133,11 +133,20 @@ class WialonUnit(WialonBase):
133
133
  **{"itemId": self.id, "phoneNumber": quote_plus(phone)}
134
134
  )
135
135
 
136
+ def clean_phone_numbers(self, phones: list[str]) -> list[str]:
137
+ cleaned_phones = []
138
+ for num in phones:
139
+ if "," in num:
140
+ cleaned_phones.extend(num.split(","))
141
+ else:
142
+ cleaned_phones.append(num)
143
+ return cleaned_phones
144
+
136
145
  def get_phone_numbers(self) -> list[str]:
137
146
  """
138
147
  Retrieves all phone numbers assigned to this Wialon unit.
139
148
 
140
- This includes the usually assigned phone number + custom/admin fields labeled ``to_number``.
149
+ This includes any attached drivers, custom fields, and normally assigned unit phone numbers.
141
150
 
142
151
  :raises WialonError: If something goes wrong with Wialon.
143
152
  :returns: A list of phone numbers.
@@ -145,8 +154,41 @@ class WialonUnit(WialonBase):
145
154
 
146
155
  """
147
156
 
148
- phones = []
149
- for field in self.cfields | self.afields:
150
- if field["n"] == "to_number":
151
- phones.append(field["v"])
152
- return phones
157
+ phone_numbers = []
158
+ phones_0 = self._get_driver_phone_numbers()
159
+ phones_1 = self._get_cfield_phone_numbers()
160
+
161
+ if phones_0:
162
+ phone_numbers.extend(phones_0)
163
+ if phones_1:
164
+ phone_numbers.extend(phones_1)
165
+ return list(dict.fromkeys(phone_numbers)) # Removes duplicate phone numbers
166
+
167
+ def _get_cfield_phone_numbers(self) -> list[str] | None:
168
+ """
169
+ Retrives any phone numbers saved in the unit's custom 'to_number' field.
170
+
171
+ :raises WialonError: If something goes wrong with Wialon.
172
+ :returns: A list of phone numbers, if the unit has custom phone number fields.
173
+ :rtype: :py:obj:`list` | :py:obj:`None`
174
+
175
+ """
176
+ for key, value in self.cfields.items():
177
+ if key == "to_number":
178
+ return self.clean_phone_numbers([value])
179
+
180
+ def _get_driver_phone_numbers(self) -> list[str] | None:
181
+ """
182
+ Retrieves any phone numbers assigned to drivers attached to the unit.
183
+
184
+ :raises WialonError: If something goes wrong with Wialon.
185
+ :returns: A list of phone numbers, if the unit has attached drivers with phone numbers.
186
+ :rtype: :py:obj:`list` | :py:obj:`None`
187
+
188
+ """
189
+ response = self.session.wialon_api.resource_get_unit_drivers(
190
+ **{"unitId": self.id}
191
+ )
192
+ if response:
193
+ dirty_phones = [driver[0].get("ph") for _, driver in response.items()]
194
+ return self.clean_phone_numbers(dirty_phones)
@@ -50,7 +50,7 @@ class WialonSession:
50
50
 
51
51
  def __enter__(self) -> "WialonSession":
52
52
  assert self.token, "Wialon API token was not set"
53
- self.login(self.token, self.login_id)
53
+ self.login(self.token)
54
54
  return self
55
55
 
56
56
  def __exit__(self, *args, **kwargs) -> None:
@@ -321,9 +321,6 @@ class WialonSessionManager:
321
321
  return cls._instance
322
322
 
323
323
  def get_session(self, sid: str | None = None) -> WialonSession:
324
- if not hasattr(settings, "WIALON_TOKEN"):
325
- raise ImproperlyConfigured("'WIALON_TOKEN' setting is required.")
326
-
327
324
  with self._lock:
328
325
  if not self._session or not self._session.active:
329
326
  self._session = WialonSession(sid=sid)
@@ -1,3 +1,4 @@
1
+ from enum import Enum
1
2
  import secrets
2
3
  import string
3
4