tinesight 0.0.2.dev1__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.
- tinesight-0.0.2.dev1/PKG-INFO +14 -0
- tinesight-0.0.2.dev1/README.md +5 -0
- tinesight-0.0.2.dev1/pyproject.toml +22 -0
- tinesight-0.0.2.dev1/src/tinesight/__init__.py +0 -0
- tinesight-0.0.2.dev1/src/tinesight/api.py +14 -0
- tinesight-0.0.2.dev1/src/tinesight/client.py +36 -0
- tinesight-0.0.2.dev1/src/tinesight/registrar.py +138 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tinesight
|
|
3
|
+
Version: 0.0.2.dev1
|
|
4
|
+
Summary: Tinesight SDK
|
|
5
|
+
Requires-Dist: cryptography>=46.0.3
|
|
6
|
+
Requires-Dist: pycognito>=2024.5.1
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# Tinesight SDK
|
|
11
|
+
|
|
12
|
+
Standalone python package for Tinesight SDK, published in the Python package `tinesight`.
|
|
13
|
+
|
|
14
|
+
Check out [SDK Documentation](http://devsdk.tinesight.com)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tinesight"
|
|
3
|
+
version = "0.0.2.dev1"
|
|
4
|
+
description = "Tinesight SDK"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"cryptography>=46.0.3",
|
|
9
|
+
"pycognito>=2024.5.1",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["uv_build"]
|
|
14
|
+
build-backend = "uv_build"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pre-commit>=4.5.0",
|
|
19
|
+
"pydata-sphinx-theme>=0.16.1",
|
|
20
|
+
"pytest>=9.0.2",
|
|
21
|
+
"sphinx>=9.0.4",
|
|
22
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TinesightApiMixin:
|
|
5
|
+
|
|
6
|
+
@property
|
|
7
|
+
def tenant_base_api_uri(self) -> str:
|
|
8
|
+
subdomain = "devapi" if os.getenv("TINESIGHT_DEV") else "api"
|
|
9
|
+
return f"https://{subdomain}.tinesight.com"
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def public_ux_api_uri(self) -> str:
|
|
13
|
+
api_ref = "p1pco7l9b1" if os.getenv("TINESIGHT_DEV") else "api"
|
|
14
|
+
return f"https://{api_ref}.execute-api.us-east-1.amazonaws.com"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import partial
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from tinesight.api import TinesightApiMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TinesightClient(TinesightApiMixin):
|
|
11
|
+
"""
|
|
12
|
+
Client for invoking the Tinesight API from a device.
|
|
13
|
+
|
|
14
|
+
To use this class, a device must be registered with a signed certificate using the
|
|
15
|
+
`TinesightRegistrar`.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
>>> result = TinesightClient(my_key_path, my_cert_path).classify(my_image_bytes)
|
|
19
|
+
>>> print(result.json)
|
|
20
|
+
>>> {'class': 'deer', 'probability': 0.98}
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def _mtls_post(self) -> Callable:
|
|
26
|
+
"""Private wrapper for making invoking requests with a certa"""
|
|
27
|
+
return partial(requests.post, cert=(self.cert_path, self.key_path))
|
|
28
|
+
|
|
29
|
+
def __init__(self, x509_key_path: Path | str, x509_cert_path: Path | str):
|
|
30
|
+
self.key_path: str = str(x509_key_path)
|
|
31
|
+
self.cert_path: str = str(x509_cert_path)
|
|
32
|
+
|
|
33
|
+
def classify(self, image_bytes: bytes) -> requests.Response:
|
|
34
|
+
"""Invokes the classification model for the specified image"""
|
|
35
|
+
classification_url = self.tenant_base_api_uri + "/classify/v1"
|
|
36
|
+
return self._mtls_post(classification_url, files={"file": image_bytes})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from cryptography import x509
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
from cryptography.hazmat.primitives import serialization
|
|
10
|
+
from cryptography.x509.oid import NameOID
|
|
11
|
+
from pycognito.utils import RequestsSrpAuth, TokenType
|
|
12
|
+
|
|
13
|
+
from tinesight.api import TinesightApiMixin
|
|
14
|
+
|
|
15
|
+
# Cognito configuration - these should be set before using TinesightRegistrar
|
|
16
|
+
# TODO (NP) figure out how to switch between dev and prod
|
|
17
|
+
COGNITO_USER_POOL_ID = os.environ.get("COGNITO_USER_POOL_ID", "us-east-1_g1yNKyU6h")
|
|
18
|
+
COGNITO_CLIENT_ID = os.environ.get("COGNITO_CLIENT_ID", "3255n0uofh58rqqiqhodtqtdp7")
|
|
19
|
+
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TinesightRegistrar(TinesightApiMixin):
|
|
23
|
+
"""
|
|
24
|
+
Represents a Tinesight tenant, with functionality for generating a signed certificate per device.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> tsr = TinesightRegistrar()
|
|
28
|
+
>>> tsr.login(my_tinesight_account_user_name, my_tinesight_account_password)
|
|
29
|
+
>>> cert = tsr.register_device(my_local_key_path, device_id)
|
|
30
|
+
>>> with open('mydevice.crt', 'w') as fp:
|
|
31
|
+
>>> fp.write(cert)
|
|
32
|
+
|
|
33
|
+
By following this example you can then instantiate a TinesightClient to invoke the Tinesight API.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, country_name: str = "US", state: str | None = None, organization: str | None = None
|
|
38
|
+
):
|
|
39
|
+
if not COGNITO_USER_POOL_ID or not COGNITO_CLIENT_ID:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"COGNITO_USER_POOL_ID and COGNITO_CLIENT_ID must be set as environment variables"
|
|
42
|
+
)
|
|
43
|
+
self.auth: RequestsSrpAuth | None = None
|
|
44
|
+
self.country_name = country_name
|
|
45
|
+
self.state = state
|
|
46
|
+
self.organization = organization
|
|
47
|
+
|
|
48
|
+
def login(self, username: str, password: str) -> "TinesightRegistrar":
|
|
49
|
+
"""
|
|
50
|
+
Basic login to using Cognito IDP with the SRP flow. This method is required to be called
|
|
51
|
+
prior to registering any devices.
|
|
52
|
+
"""
|
|
53
|
+
self.auth: RequestsSrpAuth = RequestsSrpAuth(
|
|
54
|
+
username=username,
|
|
55
|
+
password=password,
|
|
56
|
+
user_pool_id=COGNITO_USER_POOL_ID,
|
|
57
|
+
client_id=COGNITO_CLIENT_ID,
|
|
58
|
+
user_pool_region=AWS_REGION,
|
|
59
|
+
auth_token_type=TokenType.ID_TOKEN,
|
|
60
|
+
)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _read_private_key(pem_key_path: Path, key_password: str | None = None):
|
|
65
|
+
# load the private key
|
|
66
|
+
with open(pem_key_path, "rb") as fp:
|
|
67
|
+
pk_contents = fp.read()
|
|
68
|
+
return serialization.load_pem_private_key(
|
|
69
|
+
pk_contents, password=key_password.encode() if key_password else None
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def register_device(
|
|
73
|
+
self, device_id: str, pem_key_path: Path, key_password: str = None
|
|
74
|
+
) -> bytes | None:
|
|
75
|
+
"""
|
|
76
|
+
Registers a uniquely identified device for your account by creating a certificate
|
|
77
|
+
signing request and returning a signed certificate identifying your device, which will
|
|
78
|
+
enable mTLS invocation of the Tinesight API. Certificates expire after one year.
|
|
79
|
+
|
|
80
|
+
If a device with this device_id has already been registered and its certificate is not expired,
|
|
81
|
+
an exception will be thrown. If the certificate for this device is expired, a new
|
|
82
|
+
certificate will be returned.
|
|
83
|
+
|
|
84
|
+
This method requires the `TinesightRegistrar.login()` method to have been called prior
|
|
85
|
+
to executing.
|
|
86
|
+
|
|
87
|
+
:param device_id: unique device identifier
|
|
88
|
+
:param pem_key_path: path to your secret key
|
|
89
|
+
:param key_password: str, default None - password to your secret key
|
|
90
|
+
|
|
91
|
+
:return: certificate (str)
|
|
92
|
+
"""
|
|
93
|
+
if self.auth is None:
|
|
94
|
+
print("Need to call login prior to registering a device")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
key = self._read_private_key(pem_key_path, key_password)
|
|
98
|
+
|
|
99
|
+
# create a certificate signing request
|
|
100
|
+
csr = (
|
|
101
|
+
x509.CertificateSigningRequestBuilder()
|
|
102
|
+
.subject_name(
|
|
103
|
+
x509.Name(
|
|
104
|
+
[
|
|
105
|
+
# Provide various details about who we are.
|
|
106
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, self.country_name),
|
|
107
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, self.state or ""),
|
|
108
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.organization or ""),
|
|
109
|
+
x509.NameAttribute(NameOID.COMMON_NAME, device_id),
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
.add_extension(
|
|
114
|
+
x509.ExtendedKeyUsage(
|
|
115
|
+
[
|
|
116
|
+
x509.ExtendedKeyUsageOID.CLIENT_AUTH # Specifies the certificate is for client authentication
|
|
117
|
+
]
|
|
118
|
+
),
|
|
119
|
+
critical=False,
|
|
120
|
+
)
|
|
121
|
+
.add_extension(
|
|
122
|
+
x509.SubjectAlternativeName([x509.RFC822Name(self.auth.username)]), critical=False
|
|
123
|
+
)
|
|
124
|
+
.sign(key, hashes.SHA256())
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# request the signed certificate
|
|
128
|
+
target_url = self.public_ux_api_uri + "/register-device/v1"
|
|
129
|
+
response = requests.post(
|
|
130
|
+
target_url,
|
|
131
|
+
data=csr.public_bytes(serialization.Encoding.PEM),
|
|
132
|
+
auth=self.auth,
|
|
133
|
+
)
|
|
134
|
+
if response.status_code == HTTPStatus.OK:
|
|
135
|
+
json_response = response.json()
|
|
136
|
+
return json_response["certificate"].encode("utf-8")
|
|
137
|
+
else:
|
|
138
|
+
return None
|