konecty-sdk-python 2.0.5__py3-none-any.whl → 2.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.
@@ -15,6 +15,7 @@ from .http import StreamResponse
15
15
  from .http import request as _http_request
16
16
  from .serialization import json_serial
17
17
  from .services.aggregation import AggregationService
18
+ from .services.auth import AuthService
18
19
  from .services.change_user import ChangeUserService
19
20
  from .services.comments import CommentsService
20
21
  from .services.export import ExportService
@@ -52,11 +53,21 @@ def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
52
53
 
53
54
 
54
55
  class KonectyClient:
55
- def __init__(self, base_url: str, token: str) -> None:
56
+ def __init__(self, base_url: str, token: str = "") -> None:
56
57
  self.base_url = base_url
57
58
  self.headers = {"Authorization": f"{token}"}
58
59
  self.file_manager = FileManager(base_url=base_url, headers=self.headers)
59
60
 
61
+ @property
62
+ def auth_id(self) -> Optional[str]:
63
+ """Current session token (authId) sent in the Authorization header, or None."""
64
+ return self.headers.get("Authorization") or None
65
+
66
+ def set_auth_id(self, auth_id: str) -> None:
67
+ """Adopt a session token so following requests are authenticated."""
68
+ # headers is shared with file_manager: mutate in place so both see the token.
69
+ self.headers["Authorization"] = auth_id
70
+
60
71
  async def _request(
61
72
  self,
62
73
  method: str,
@@ -361,6 +372,51 @@ class KonectyClient:
361
372
  """Set queue on records."""
362
373
  return await self._change_user.set_queue(module, ids, queue)
363
374
 
375
+ @property
376
+ def _auth(self) -> AuthService:
377
+ if not hasattr(self, "_auth_service"):
378
+ self._auth_service = AuthService(self)
379
+ return self._auth_service
380
+
381
+ def google_login_url(
382
+ self,
383
+ client_id: str,
384
+ redirect_uri: Optional[str] = None,
385
+ state: Optional[str] = None,
386
+ ) -> str:
387
+ """Build the URL of GET /api/auth/google/start. Pure, performs no request."""
388
+ return self._auth.google_login_url(
389
+ client_id, redirect_uri=redirect_uri, state=state
390
+ )
391
+
392
+ async def exchange_google_code(
393
+ self,
394
+ code: str,
395
+ *,
396
+ geolocation: Optional[Union[Dict[str, Any], str]] = None,
397
+ resolution: Optional[Union[Dict[str, Any], str]] = None,
398
+ source: Optional[str] = None,
399
+ fingerprint: Optional[str] = None,
400
+ ) -> Dict[str, Any]:
401
+ """
402
+ Exchange the single-use code from the callback for a session
403
+ (POST /api/auth/google/session).
404
+
405
+ On success adopts the returned authId in this client. On error raises
406
+ KonectyAPIError with errors[0].message and keeps the client unauthenticated.
407
+ """
408
+ return await self._auth.exchange_google_code(
409
+ code,
410
+ geolocation=geolocation,
411
+ resolution=resolution,
412
+ source=source,
413
+ fingerprint=fingerprint,
414
+ )
415
+
416
+ async def get_login_options(self) -> Dict[str, Any]:
417
+ """Get login method flags for the namespace (GET /api/auth/login-options)."""
418
+ return await self._auth.get_login_options()
419
+
364
420
  @property
365
421
  def _query(self) -> QueryService:
366
422
  if not hasattr(self, "_query_service"):
@@ -0,0 +1,105 @@
1
+ """Auth API: Google hosted authorization code flow and login options."""
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+ from urllib.parse import quote, urlencode
5
+
6
+ from ..exceptions import KonectyAPIError
7
+ from .base import BaseService
8
+
9
+ GOOGLE_START_PATH = "/api/auth/google/start"
10
+ # Conjunto que `encodeURIComponent` deixa passar além de [A-Za-z0-9_.-~], que o
11
+ # `quote` já preserva por padrão. Usado para que a URL montada aqui seja
12
+ # byte a byte igual à do SDK TypeScript — ver `_encode_query` abaixo.
13
+ _ENCODE_URI_COMPONENT_SAFE = "!*'()"
14
+ GOOGLE_SESSION_PATH = "/api/auth/google/session"
15
+ LOGIN_OPTIONS_PATH = "/api/auth/login-options"
16
+
17
+
18
+ def _first_error_message(error: KonectyAPIError) -> str:
19
+ """Extract errors[0].message from a KonectyAPIError raised by the HTTP layer."""
20
+ errors = error.args[0] if error.args else None
21
+ if isinstance(errors, list) and errors:
22
+ first = errors[0]
23
+ if isinstance(first, dict):
24
+ message = first.get("message")
25
+ if isinstance(message, str) and message:
26
+ return message
27
+ return str(error)
28
+
29
+
30
+ def _encode_query(params: list) -> str:
31
+ """
32
+ Codifica o query string como `encodeURIComponent` do SDK TypeScript.
33
+
34
+ O default do `urlencode` é `quote_plus`, que transforma espaço em `+`;
35
+ `encodeURIComponent` produz `%20`. Os dois decodificam para o mesmo valor no
36
+ servidor, mas a spec pede URLs idênticas entre os SDKs.
37
+ """
38
+ return urlencode(params, quote_via=quote, safe=_ENCODE_URI_COMPONENT_SAFE)
39
+
40
+
41
+ class AuthService(BaseService):
42
+ """Service for the Google login flow (authorization code hosted by Konecty)."""
43
+
44
+ def google_login_url(
45
+ self,
46
+ client_id: str,
47
+ redirect_uri: Optional[str] = None,
48
+ state: Optional[str] = None,
49
+ ) -> str:
50
+ """
51
+ Build the absolute URL of GET /api/auth/google/start. Does not perform any request.
52
+
53
+ The browser follows this URL; Konecty redirects to accounts.google.com and,
54
+ after consent, back to redirect_uri with a single-use code (and the original state).
55
+ """
56
+ params = [("client_id", client_id)]
57
+ if redirect_uri is not None:
58
+ params.append(("redirect_uri", redirect_uri))
59
+ if state is not None:
60
+ params.append(("state", state))
61
+ base_url = self._client.base_url.rstrip("/")
62
+ return f"{base_url}{GOOGLE_START_PATH}?{_encode_query(params)}"
63
+
64
+ async def exchange_google_code(
65
+ self,
66
+ code: str,
67
+ *,
68
+ geolocation: Optional[Union[Dict[str, Any], str]] = None,
69
+ resolution: Optional[Union[Dict[str, Any], str]] = None,
70
+ source: Optional[str] = None,
71
+ fingerprint: Optional[str] = None,
72
+ ) -> Dict[str, Any]:
73
+ """
74
+ POST /api/auth/google/session. Exchanges the single-use code (TTL 60s) for a session.
75
+
76
+ On success the returned authId is adopted by the client, so following requests
77
+ are authenticated. On error raises KonectyAPIError with errors[0].message and
78
+ leaves the client authentication state untouched.
79
+ """
80
+ payload: Dict[str, Any] = {"code": code}
81
+ if geolocation is not None:
82
+ payload["geolocation"] = geolocation
83
+ if resolution is not None:
84
+ payload["resolution"] = resolution
85
+ if source is not None:
86
+ payload["source"] = source
87
+ if fingerprint is not None:
88
+ payload["fingerprint"] = fingerprint
89
+
90
+ try:
91
+ response = await self._post(GOOGLE_SESSION_PATH, json=payload)
92
+ except KonectyAPIError as error:
93
+ raise KonectyAPIError(_first_error_message(error)) from error
94
+
95
+ auth_id = response.get("authId") if isinstance(response, dict) else None
96
+ if not isinstance(auth_id, str) or not auth_id:
97
+ raise KonectyAPIError("Google session response has no authId")
98
+
99
+ self._client.set_auth_id(auth_id)
100
+ return response
101
+
102
+ async def get_login_options(self) -> Dict[str, Any]:
103
+ """GET /api/auth/login-options. Flags: passwordEnabled, emailOtpEnabled,
104
+ whatsAppOtpEnabled, webauthnEnabled, webauthnRequired, googleEnabled."""
105
+ return await self._get(LOGIN_OPTIONS_PATH)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: konecty_sdk_python
3
+ Version: 2.1.0
4
+ Summary: Konecty SDK Python
5
+ Author-email: Leonardo Leal <leonardo.leal@konecty.com>, Derotino Silveira <derotino.silveira@konecty.com>
6
+ License: MIT
7
+ Keywords: api,konecty,sdk
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: aiohttp>=3.11.11
17
+ Requires-Dist: asyncio>=3.4.3
18
+ Requires-Dist: black>=24.10.0
19
+ Requires-Dist: email-validator>=2.2.0
20
+ Requires-Dist: inquirer>=3.4.0
21
+ Requires-Dist: pydantic>=2.11.4
22
+ Requires-Dist: pymongo>=4.10.1
23
+ Requires-Dist: requests>=2.32.3
24
+ Requires-Dist: rich>=13.9.4
25
+ Requires-Dist: typing-extensions>=4.12.2
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ ## Konecty Python SDK
32
+
33
+ > 🛠️ Work in progress
34
+
35
+ This project exposes both:
36
+
37
+ - a cli for interacting with the database
38
+ - the Konecty client sdk for interacting with Konecty's api
39
+
40
+ #### Usage
41
+
42
+ ##### Installing on a project
43
+
44
+ ```sh
45
+ uv pip install konecty-sdk-python
46
+ konecty-cli apply --mongo-url="..." --database my-db
47
+ ```
48
+
49
+ ##### Running on uvx
50
+
51
+ ```sh
52
+ uvx --from konecty-sdk-python konecty-cli pull --all --mongo-url="..." --database my-db
53
+ ```
54
+
55
+ #### Google login
56
+
57
+ The Konecty core hosts the whole authorization code flow: your app never talks to Google
58
+ and never sees `client_secret`. The `authId` never travels in a URL — the callback returns
59
+ a single-use code (TTL 60s) and the `authId` only exists in the body of `POST /api/auth/google/session`.
60
+
61
+ Three steps, three methods:
62
+
63
+ 1. `client.google_login_url(...)` — build the URL to send the browser to (no request is made).
64
+ 2. the browser comes back to your `redirect_uri` with `?code=...&state=...` (or `?error=...&state=...`).
65
+ 3. `await client.exchange_google_code(code)` — turn the code into a session; the returned
66
+ `authId` is adopted by the client, so the next calls are authenticated.
67
+
68
+ ```python
69
+ import asyncio
70
+ import os
71
+ import secrets
72
+
73
+ from KonectySdkPython import KonectyClient
74
+ from KonectySdkPython.lib.exceptions import KonectyAPIError
75
+
76
+ KONECTY_URL = os.environ["KONECTY_URL"]
77
+ CLIENT_ID = os.environ["KONECTY_CLIENT_ID"]
78
+ REDIRECT_URI = os.environ["KONECTY_REDIRECT_URI"]
79
+
80
+
81
+ async def main() -> None:
82
+ client = KonectyClient(KONECTY_URL)
83
+
84
+ # Is Google login enabled for this namespace?
85
+ options = await client.get_login_options()
86
+ if not options["googleEnabled"]:
87
+ raise SystemExit("Google login is disabled for this namespace")
88
+
89
+ # Step 1 — send the browser here (state is opaque to Konecty; use it against CSRF).
90
+ state = secrets.token_urlsafe(16)
91
+ print(client.google_login_url(client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, state=state))
92
+
93
+ # Step 2 — Konecty redirects the browser back to REDIRECT_URI with code + state.
94
+ # On refusal it comes back as ?error=<access_denied|provider_error|
95
+ # email_not_verified|user_not_found|user_inactive|ambiguous_user>&state=...
96
+ returned_code = input("code from the callback: ")
97
+
98
+ # Step 3 — exchange the single-use code for a session.
99
+ try:
100
+ session = await client.exchange_google_code(
101
+ returned_code,
102
+ source="my-app", # optional telemetry: geolocation, resolution, source, fingerprint
103
+ )
104
+ except KonectyAPIError as error:
105
+ raise SystemExit(f"Google login failed: {error}")
106
+
107
+ print("logged as", session["user"]["login"], "authId:", client.auth_id)
108
+
109
+ # The client is now authenticated: no token needed on the constructor.
110
+ contacts = await client.find_by_id("User", session["user"]["_id"])
111
+ print(contacts)
112
+
113
+
114
+ asyncio.run(main())
115
+ ```
116
+
117
+ `exchange_google_code` raises `KonectyAPIError` with the message of `errors[0]`
118
+ (`invalid_code`, `expired_code`, `user_not_found`, `user_inactive`) and leaves the client's
119
+ authentication state untouched.
120
+
121
+ #### Tests
122
+
123
+ ```sh
124
+ uv pip install -e ".[dev]"
125
+ .venv/bin/python -m pytest
126
+ ```
127
+
128
+ #### Build & Publish
129
+
130
+ It is needed to increase the version number on the [pyproject](./pyproject.toml) file.
131
+
132
+ ```sh
133
+ uv build
134
+ uvx twine upload --config-file .pypirc --skip-existing dist/*
135
+ ```
@@ -4,7 +4,7 @@ KonectySdkPython/cli/apply.py,sha256=LyPoNcsU3oz9P_eTAi4BdtJHWkbp-sNK_SP9W8Xua04
4
4
  KonectySdkPython/cli/backup.py,sha256=rcla3EWfPsy-vWgmDK844EaM5u3h7JF-u7-b2HUBMgA,4785
5
5
  KonectySdkPython/cli/pull.py,sha256=n4gIkKtuEpPKULb3zcYu5tZlBLbt_4oNQDeu2l6tHxU,9529
6
6
  KonectySdkPython/lib/__init__.py,sha256=Du8jLNfFG1bQuHX5u7pAhbyV9TjC7Ie9K1Xo1KSUqqc,995
7
- KonectySdkPython/lib/client.py,sha256=jX1_ayUYF_vMQuCj0Kuk1Fhw3SYie-RJ8Ekna9uUWSo,31954
7
+ KonectySdkPython/lib/client.py,sha256=i-7goKqsH-jvwxZfGeipTRFByOBAOhMHkuhjjdEN3kE,34034
8
8
  KonectySdkPython/lib/exceptions.py,sha256=HR48cd-cptFPoObr37NcvDrdkb-RtxKRteQoG-tuPkk,529
9
9
  KonectySdkPython/lib/file_manager.py,sha256=CLpOJWdA1tb2iq0qOIeRyEEIuACRq9tIo7Rls1YT2ig,9986
10
10
  KonectySdkPython/lib/filters.py,sha256=PsFaUFCEyKgxgzD566LdTLvuMJevoF_i9U1WnpJZzPQ,4482
@@ -19,6 +19,7 @@ KonectySdkPython/lib/feature_types/kpi.py,sha256=67RpwwwnzU8piIPRyFb6Kn0hqSRpT9R
19
19
  KonectySdkPython/lib/feature_types/query_json.py,sha256=yZgMPmkMdiBLm-dIrmU1AeZe5G9zQLwLtKUPMb7IOhU,8139
20
20
  KonectySdkPython/lib/services/__init__.py,sha256=fluQuxs6LnIZwcjYTuKNeXCFTKuNF3At3X19fIB68p8,121
21
21
  KonectySdkPython/lib/services/aggregation.py,sha256=igK1mleMFU2_rDQCAU1yKyvDoOjhTO1SO0Nn4T_DokA,5610
22
+ KonectySdkPython/lib/services/auth.py,sha256=JFiOvk0cqwRucvDQCN93jmKvSITxGtFYW1ME0gdEkMk,4226
22
23
  KonectySdkPython/lib/services/base.py,sha256=tdVyWb7Y77cBhVc8Mz2BQMN6kAyZIohoxBxdwDnRsYs,1820
23
24
  KonectySdkPython/lib/services/change_user.py,sha256=ce6mcDYMP6eWxW0bEtzlVf7wbppGHCkGwdgo2jJFJOQ,2548
24
25
  KonectySdkPython/lib/services/comments.py,sha256=RNevCg4de2FWNk7mD60-pk3r8f2sJ1NLoolD2iNMidg,3033
@@ -28,7 +29,7 @@ KonectySdkPython/lib/services/notifications.py,sha256=7AtCBbBl-YAz_RBIW6bzoKvuws
28
29
  KonectySdkPython/lib/services/query.py,sha256=vCqbf0QdV8Blh8Hfsvq6Ne1-YFuC5siw1gVa7ZBGs_k,6605
29
30
  KonectySdkPython/lib/services/stream.py,sha256=1DMVxcg9Ymk2NgX0Qfvpjrr6vZJo5iFovPFT1NA3_CY,4737
30
31
  KonectySdkPython/lib/services/subscriptions.py,sha256=_Jq8jW7YkL_hwLNte1bWgrr4D2EMZUUSOXUgnRoGGK4,1022
31
- konecty_sdk_python-2.0.5.dist-info/METADATA,sha256=GNN5r6m-JKJaOiwghqtS-IscpxYTmQB-Or5JMgI3PeA,1622
32
- konecty_sdk_python-2.0.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
33
- konecty_sdk_python-2.0.5.dist-info/entry_points.txt,sha256=awf962DamFgy791G3PsJ354edBMLJg16Dqi-2Y1tb9s,58
34
- konecty_sdk_python-2.0.5.dist-info/RECORD,,
32
+ konecty_sdk_python-2.1.0.dist-info/METADATA,sha256=bDqiKz0B699Mulju2AhOkSboeJIQetZBxCgtissnnNI,4454
33
+ konecty_sdk_python-2.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
34
+ konecty_sdk_python-2.1.0.dist-info/entry_points.txt,sha256=awf962DamFgy791G3PsJ354edBMLJg16Dqi-2Y1tb9s,58
35
+ konecty_sdk_python-2.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,59 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: konecty_sdk_python
3
- Version: 2.0.5
4
- Summary: Konecty SDK Python
5
- Author-email: Leonardo Leal <leonardo.leal@konecty.com>, Derotino Silveira <derotino.silveira@konecty.com>
6
- License: MIT
7
- Keywords: api,konecty,sdk
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
- Requires-Python: >=3.11
16
- Requires-Dist: aiohttp>=3.11.11
17
- Requires-Dist: asyncio>=3.4.3
18
- Requires-Dist: black>=24.10.0
19
- Requires-Dist: email-validator>=2.2.0
20
- Requires-Dist: inquirer>=3.4.0
21
- Requires-Dist: pydantic>=2.11.4
22
- Requires-Dist: pymongo>=4.10.1
23
- Requires-Dist: requests>=2.32.3
24
- Requires-Dist: rich>=13.9.4
25
- Requires-Dist: typing-extensions>=4.12.2
26
- Description-Content-Type: text/markdown
27
-
28
- ## Konecty Python SDK
29
-
30
- > 🛠️ Work in progress
31
-
32
- This project exposes both:
33
-
34
- - a cli for interacting with the database
35
- - the Konecty client sdk for interacting with Konecty's api
36
-
37
- #### Usage
38
-
39
- ##### Installing on a project
40
-
41
- ```sh
42
- uv pip install konecty-sdk-python
43
- konecty-cli apply --mongo-url="..." --database my-db
44
- ```
45
-
46
- ##### Running on uvx
47
-
48
- ```sh
49
- uvx --from konecty-sdk-python konecty-cli pull --all --mongo-url="..." --database my-db
50
- ```
51
-
52
- #### Build & Publish
53
-
54
- It is needed to increase the version number on the [pyproject](./pyproject.toml) file.
55
-
56
- ```sh
57
- uv build
58
- uvx twine upload --config-file .pypirc --skip-existing dist/*
59
- ```