pyituran 0.1.0__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.
pyituran/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """The pyituran library."""
2
+
3
+ from pyituran.ituran import Ituran
4
+
5
+ __all__ = ["Ituran"]
pyituran/cmdline.py ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python
2
+
3
+ import argparse
4
+ import asyncio
5
+ import sys
6
+ from pyituran import Ituran
7
+
8
+
9
+ async def async_main(args=None):
10
+ if args is None:
11
+ args = sys.argv[1:]
12
+
13
+ parser = argparse.ArgumentParser(
14
+ description="Ituran command line tool.\n"
15
+ + "When executing this tool, please provide a phone number on"
16
+ + "first use or a mobile ID if already authenticated",
17
+ formatter_class=argparse.RawTextHelpFormatter,
18
+ )
19
+ parser.add_argument(
20
+ "--id-number",
21
+ action="store",
22
+ dest="id_number",
23
+ default=None,
24
+ help="ID number of the account owner",
25
+ required=True,
26
+ )
27
+ parser.add_argument(
28
+ "--phone-number",
29
+ action="store",
30
+ dest="phone_number",
31
+ default="",
32
+ help="Phone number of the account owner, required for OTP",
33
+ )
34
+ parser.add_argument(
35
+ "--mobile-id",
36
+ action="store",
37
+ dest="mobile_id",
38
+ default=None,
39
+ help="A unique, 16 hex-digit, ID for this client. "
40
+ + "Once authenticated via OTP, it must remain constant",
41
+ )
42
+ args = parser.parse_args(args)
43
+
44
+ if not (args.phone_number or args.mobile_id):
45
+ parser.error("Must provide either a phone number or a mobile ID")
46
+
47
+ ituran = Ituran(args.id_number, args.phone_number, args.mobile_id)
48
+ if not await ituran.is_authenticated():
49
+ print("The provided credentials aren't authenticated.")
50
+ response = input(
51
+ f"Would you like to authenticate now with ID '{args.id_number}'"
52
+ + f" and phone number '{args.phone_number}' (y/n)? "
53
+ )
54
+ if response != "y":
55
+ return
56
+ try:
57
+ await ituran.request_otp()
58
+ except Exception:
59
+ print("Failed requesting OTP, please verify ID and phone number")
60
+ return
61
+ print("OTP request sent.")
62
+ while True:
63
+ otp = input("Please input the received one-time code: ")
64
+ try:
65
+ await ituran.authenticate(otp)
66
+ except:
67
+ print("Incorrect code, please try again.")
68
+ else:
69
+ break
70
+ print(
71
+ "Success!\n"
72
+ + f"Please keep the following mobile ID for future use: {ituran.mobile_id}"
73
+ )
74
+
75
+ vehicles = await ituran.get_vehicles()
76
+ for vehicle in vehicles:
77
+ print(
78
+ f"License plate: {vehicle.license_plate}:\n"
79
+ + f"\tMake: {vehicle.make}\n"
80
+ + f"\tModel: {vehicle.model}\n"
81
+ + f"\tLocation: {vehicle.gps_coordinates}\n"
82
+ + f"\tAddress: {vehicle.address}\n"
83
+ + f"\tHeading: {vehicle.heading}\n"
84
+ + f"\tSpeed: {vehicle.speed}\n"
85
+ + f"\tMileage: {vehicle.mileage}\n"
86
+ + f"\tLast update: {vehicle.last_update}\n"
87
+ )
88
+
89
+
90
+ def main(args=None):
91
+ asyncio.run(async_main(args))
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main() # pragma: no cover
pyituran/const.py ADDED
@@ -0,0 +1,32 @@
1
+ """Ituran library constants."""
2
+
3
+ DOMAIN = "https://www.ituran.com"
4
+ SOAP_SERVICES = DOMAIN + "/SoapService"
5
+ ACTIVATION_URL = SOAP_SERVICES + "/APPApi.asmx/AppActivation"
6
+ OTP_VERIFICATION_URL = (
7
+ SOAP_SERVICES + "/ituran4all.asmx/AppSerializationRequest"
8
+ )
9
+ ITURAN_GET_VEHICLES_URL = (
10
+ DOMAIN + "/ituranmobileservice/mobileservice.asmx/GetUserPlatforms"
11
+ )
12
+
13
+ TEMP_NAMESPACE = "http://tempuri.org/"
14
+ XML_RESPONSE_STATUS = f"{{{TEMP_NAMESPACE}}}ResponseStatus"
15
+
16
+ IMS_NAMESPACE = "http://www.ituran.com/IturanMobileService"
17
+ XML_RETURN_CODE = f"{{{IMS_NAMESPACE}}}ReturnCode"
18
+ XML_ERROR_DESCRIPTION = f"{{{IMS_NAMESPACE}}}ErrorDescription"
19
+ XML_VEHICLES_LIST = f"{{{IMS_NAMESPACE}}}VehList"
20
+ XML_VEHICLE_MODEL = f"{{{IMS_NAMESPACE}}}Model"
21
+ XML_VEHICLE_MAKE = f"{{{IMS_NAMESPACE}}}Make"
22
+ XML_VEHICLE_PLATE = f"{{{IMS_NAMESPACE}}}Plate"
23
+ XML_VEHICLE_LATITUTE = f"{{{IMS_NAMESPACE}}}Lat"
24
+ XML_VEHICLE_LONGITUDE = f"{{{IMS_NAMESPACE}}}Lon"
25
+ XML_VEHICLE_ADDRESS = f"{{{IMS_NAMESPACE}}}Address"
26
+ XML_VEHICLE_SPEED = f"{{{IMS_NAMESPACE}}}Speed"
27
+ XML_VEHICLE_HEADING = f"{{{IMS_NAMESPACE}}}Head"
28
+ XML_VEHICLE_LAST_MILEAGE = f"{{{IMS_NAMESPACE}}}LastMileage"
29
+ XML_VEHICLE_UPDATE_DATE = f"{{{IMS_NAMESPACE}}}Date"
30
+
31
+ ERROR_OK = "ok"
32
+ ERROR_INVALID_CREDENTIALS = "IncorrectUserNameOrPassword"
pyituran/ituran.py ADDED
@@ -0,0 +1,146 @@
1
+ """Module client for the Ituran web service."""
2
+
3
+ import aiohttp
4
+ from aiohttp import FormData
5
+ import logging
6
+ from typing import List, Optional
7
+ import uuid
8
+ import xml.etree.ElementTree as ElementTree
9
+
10
+ from pyituran.const import (
11
+ ERROR_INVALID_CREDENTIALS,
12
+ ERROR_OK,
13
+ ACTIVATION_URL,
14
+ ITURAN_GET_VEHICLES_URL,
15
+ OTP_VERIFICATION_URL,
16
+ XML_ERROR_DESCRIPTION,
17
+ XML_RESPONSE_STATUS,
18
+ XML_RETURN_CODE,
19
+ XML_VEHICLES_LIST,
20
+ )
21
+ from pyituran.vehicle import Vehicle
22
+
23
+
24
+ logger = logging.getLogger(__package__)
25
+
26
+
27
+ class Ituran:
28
+ def __init__(
29
+ self,
30
+ id_number: str,
31
+ phone_number: str,
32
+ mobile_id: Optional[str] = None,
33
+ ) -> None:
34
+ assert id_number is not None
35
+ assert phone_number is not None
36
+ self.__id_number = id_number
37
+ self.__phone_number = phone_number
38
+ self.__mobile_id = (
39
+ mobile_id if mobile_id is not None else self.__generate_mobile_id()
40
+ )
41
+
42
+ async def is_authenticated(self) -> bool:
43
+ """Tests if current credentials are valid."""
44
+ try:
45
+ _ = await self.get_vehicles()
46
+ return True
47
+ except Exception as e:
48
+ if len(e.args) > 0 and e.args[0] == ERROR_INVALID_CREDENTIALS:
49
+ return False
50
+ raise
51
+
52
+ async def request_otp(self) -> bool:
53
+ data = FormData()
54
+ data.add_field("UserName", self.__id_number)
55
+ data.add_field("SiebelPassword", self.__phone_number)
56
+ data.add_field("AppId", 49)
57
+ data.add_field("OSType", "Android")
58
+ try:
59
+ async with aiohttp.ClientSession() as session:
60
+ logger.debug("Requesting OTP")
61
+ async with session.post(ACTIVATION_URL, data=data) as response:
62
+ response_data = await response.text()
63
+ logger.debug(f"Got {response.status}: {response_data}")
64
+ assert response.status == 200
65
+ root = ElementTree.fromstring(response_data)
66
+ response_status = root.find(XML_RESPONSE_STATUS)
67
+ assert response_status is not None
68
+ if response_status.text != ERROR_OK:
69
+ raise Exception(response_status.text)
70
+ except Exception as e:
71
+ logging.error(f"Failed requesting OTP: {e}")
72
+ raise
73
+ return True
74
+
75
+ async def authenticate(self, otp: str) -> bool:
76
+ data = FormData()
77
+ data.add_field("UserName", self.__id_number)
78
+ data.add_field("SiebelPassword", self.__phone_number)
79
+ data.add_field("OTPcode", otp)
80
+ data.add_field("AppId", 49)
81
+ data.add_field("MobileId", self.__mobile_id)
82
+ try:
83
+ async with aiohttp.ClientSession() as session:
84
+ logger.debug(f"Authenticating with OTP {otp}")
85
+ async with session.post(
86
+ OTP_VERIFICATION_URL, data=data
87
+ ) as response:
88
+ response_data = await response.text()
89
+ logger.debug(f"Got {response.status}: {response_data}")
90
+ root = ElementTree.fromstring(response_data)
91
+ response_status = root.find(XML_RESPONSE_STATUS)
92
+ assert response_status is not None
93
+ if response_status.text != ERROR_OK:
94
+ raise Exception(response_status.text)
95
+ except Exception as e:
96
+ logging.error(f"Failed requesting OTP: {e}")
97
+ raise
98
+ return True
99
+
100
+ async def get_vehicles(self) -> List[Vehicle]:
101
+ vehicles: List[Vehicle] = []
102
+ try:
103
+ root = await self.__get_vehicles_xml()
104
+ error = self.__get_error_from_response(root)
105
+ if error is not None:
106
+ raise Exception(error)
107
+ vehicles_list = root.find(XML_VEHICLES_LIST)
108
+ assert vehicles_list is not None
109
+ for vehicle in vehicles_list:
110
+ vehicles.append(Vehicle(vehicle))
111
+ except Exception as e:
112
+ logging.error(f"Failed getting list of vehicles: {e}")
113
+ raise
114
+ return vehicles
115
+
116
+ @property
117
+ def mobile_id(self):
118
+ return self.__mobile_id
119
+
120
+ async def __get_vehicles_xml(self) -> ElementTree.Element:
121
+ data = FormData()
122
+ data.add_field("UserName", self.__id_number)
123
+ data.add_field("GetAddress", True)
124
+ data.add_field("Password", self.__mobile_id)
125
+ async with aiohttp.ClientSession() as session:
126
+ logger.debug("Getting list of vehicles")
127
+ async with session.post(
128
+ ITURAN_GET_VEHICLES_URL, data=data
129
+ ) as response:
130
+ response_data = await response.text()
131
+ assert response.status == 200
132
+ logger.debug(f"Got {response.status}: {response_data}")
133
+ return ElementTree.fromstring(response_data)
134
+
135
+ def __get_error_from_response(
136
+ self, xml: ElementTree.Element
137
+ ) -> Optional[str]:
138
+ return_code = xml.find(XML_RETURN_CODE)
139
+ error_description = xml.find(XML_ERROR_DESCRIPTION)
140
+ assert return_code is not None and error_description is not None
141
+ if return_code.text == ERROR_OK.upper():
142
+ return None
143
+ return error_description.text
144
+
145
+ def __generate_mobile_id(self) -> str:
146
+ return uuid.uuid4().hex[:16]
pyituran/vehicle.py ADDED
@@ -0,0 +1,94 @@
1
+ """Class representing an Ituran vehicle."""
2
+
3
+ from datetime import datetime
4
+ import logging
5
+ from typing import Tuple
6
+ import xml.etree.ElementTree as ElementTree
7
+
8
+ from pyituran.const import (
9
+ XML_VEHICLE_MODEL,
10
+ XML_VEHICLE_MAKE,
11
+ XML_VEHICLE_PLATE,
12
+ XML_VEHICLE_LATITUTE,
13
+ XML_VEHICLE_LONGITUDE,
14
+ XML_VEHICLE_ADDRESS,
15
+ XML_VEHICLE_SPEED,
16
+ XML_VEHICLE_HEADING,
17
+ XML_VEHICLE_LAST_MILEAGE,
18
+ XML_VEHICLE_UPDATE_DATE,
19
+ )
20
+
21
+
22
+ logger = logging.getLogger(__package__)
23
+
24
+
25
+ class Vehicle:
26
+ def __init__(self, xml: ElementTree.Element) -> None:
27
+ self.__make: str = self.__xml_get_field(xml, XML_VEHICLE_MAKE)
28
+ self.__model: str = self.__xml_get_field(xml, XML_VEHICLE_MODEL)
29
+ self.__license_plate: str = self.__xml_get_field(
30
+ xml, XML_VEHICLE_PLATE
31
+ )
32
+ self.__latitue: float = float(
33
+ self.__xml_get_field(xml, XML_VEHICLE_LATITUTE)
34
+ )
35
+ self.__longitude: float = float(
36
+ self.__xml_get_field(xml, XML_VEHICLE_LONGITUDE)
37
+ )
38
+ self.__address: str = self.__xml_get_field(xml, XML_VEHICLE_ADDRESS)
39
+ self.__speed: int = int(self.__xml_get_field(xml, XML_VEHICLE_SPEED))
40
+ self.__heading: int = int(
41
+ self.__xml_get_field(xml, XML_VEHICLE_HEADING)
42
+ )
43
+ self.__mileage: float = float(
44
+ self.__xml_get_field(xml, XML_VEHICLE_LAST_MILEAGE)
45
+ )
46
+ self.__last_update: datetime = datetime.fromisoformat(
47
+ self.__xml_get_field(xml, XML_VEHICLE_UPDATE_DATE)
48
+ )
49
+
50
+ @property
51
+ def make(self) -> str:
52
+ return self.__make
53
+
54
+ @property
55
+ def model(self) -> str:
56
+ return self.__model
57
+
58
+ @property
59
+ def license_plate(self) -> str:
60
+ return self.__license_plate
61
+
62
+ @property
63
+ def gps_coordinates(self) -> Tuple[float, float]:
64
+ return (self.__latitue, self.__longitude)
65
+
66
+ @property
67
+ def address(self) -> str:
68
+ return self.__address
69
+
70
+ @property
71
+ def heading(self) -> int:
72
+ return self.__heading
73
+
74
+ @property
75
+ def speed(self) -> int:
76
+ return self.__speed
77
+
78
+ @property
79
+ def mileage(self) -> float:
80
+ return self.__mileage
81
+
82
+ @property
83
+ def last_update(self) -> datetime:
84
+ return self.__last_update
85
+
86
+ def __str__(self) -> str:
87
+ return f"{self.make} {self.model} @ {self.gps_coordinates}"
88
+
89
+ def __xml_get_field(self, xml: ElementTree.Element, path: str) -> str:
90
+ element = xml.find(path)
91
+ assert element is not None
92
+ text = element.text
93
+ assert text is not None
94
+ return text
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyituran
3
+ Version: 0.1.0
4
+ Summary: A module to interact with Ituran's web service.
5
+ Home-page: https://github.com/shmuelzon/pyituran
6
+ Author: Shmuelzon
7
+ License: MIT
8
+ Download-URL: https://github.com/shmuelzon/pyituran/tarball/0.1.0
9
+ Platform: UNKNOWN
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: aiohttp
13
+ Requires-Dist: asyncio
14
+ Provides-Extra: test
15
+ Requires-Dist: black; extra == "test"
16
+ Requires-Dist: coverage; extra == "test"
17
+ Requires-Dist: flake8; extra == "test"
18
+ Requires-Dist: pep8-naming; extra == "test"
19
+ Requires-Dist: pytest; extra == "test"
20
+ Requires-Dist: pytest-asyncio; extra == "test"
21
+ Requires-Dist: pytest-cov; extra == "test"
22
+ Requires-Dist: setuptools; extra == "test"
23
+ Requires-Dist: wheel; extra == "test"
24
+
25
+ # pyituran
26
+
27
+
@@ -0,0 +1,10 @@
1
+ pyituran/__init__.py,sha256=oBzbaNbraC4gg3BZ_8_hEEXJP2WBXpZPNC0KwiTYfpg,86
2
+ pyituran/cmdline.py,sha256=9R5ha5YHb_EPFWiNo-f_wuW4xWd0PW0vnQuPO1uUpbs,2949
3
+ pyituran/const.py,sha256=JGtj1rpn4d0xcD7iyWoyYcqCXzDChfNVVgkHIr3zous,1265
4
+ pyituran/ituran.py,sha256=EPwb97V797Glh4gqZ7YrxIayRNU-S07ccMiSWRuspAM,5246
5
+ pyituran/vehicle.py,sha256=3moz0HufyKIwXI-MwHoF_iacz9pgEFWa5n48TBZno8w,2592
6
+ pyituran-0.1.0.dist-info/METADATA,sha256=oMBgiT-xRDqkzK9a8O_p3V8ZIDbhUiParg8m568oEdc,789
7
+ pyituran-0.1.0.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
8
+ pyituran-0.1.0.dist-info/entry_points.txt,sha256=VuU9w2Kxniw9SS_pfoPbYEfrmNhRby-8Vd-GXVCwdP8,50
9
+ pyituran-0.1.0.dist-info/top_level.txt,sha256=a_BX035kA9RJcpur2iKFZc6Lcw5i1PExPPTlNi1AWU4,9
10
+ pyituran-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.44.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ituran = pyituran.cmdline:main
3
+
@@ -0,0 +1 @@
1
+ pyituran