kavchat 0.0.4__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.
kavchat/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Kavchat server SDK: identity tokens, webhook verification and the Server API."""
2
+ from .client import DEFAULT_BASE_URL, VERSION, Kavchat
3
+ from .errors import KavchatApiError, KavchatError, WebhookVerificationError
4
+ from .identity import DEFAULT_TOKEN_TTL_SECONDS, MAX_TOKEN_TTL_SECONDS, create_user_token
5
+ from .webhook import SIGNATURE_HEADER, verify_webhook
6
+
7
+ __version__ = VERSION
8
+ __all__ = [
9
+ "Kavchat",
10
+ "KavchatError",
11
+ "KavchatApiError",
12
+ "WebhookVerificationError",
13
+ "create_user_token",
14
+ "verify_webhook",
15
+ "DEFAULT_BASE_URL",
16
+ "DEFAULT_TOKEN_TTL_SECONDS",
17
+ "MAX_TOKEN_TTL_SECONDS",
18
+ "SIGNATURE_HEADER",
19
+ ]
kavchat/client.py ADDED
@@ -0,0 +1,160 @@
1
+ """Kavchat Server API client (stdlib only)."""
2
+ import json
3
+ import socket
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, List, Optional, Union, cast
9
+
10
+ from .errors import KavchatApiError
11
+ from .identity import DEFAULT_TOKEN_TTL_SECONDS, create_user_token
12
+ from .types import Audience, ReleaseSummary, Report, Settings, Ticket, TicketStatus, WebhookEvent, Workspace
13
+ from .webhook import DEFAULT_TOLERANCE_SECONDS, verify_webhook
14
+
15
+ DEFAULT_BASE_URL = "https://api.kavchat.com"
16
+ VERSION = "0.0.4"
17
+
18
+ DateLike = Union[datetime, str]
19
+
20
+
21
+ def _iso(value: Optional[DateLike]) -> Optional[str]:
22
+ if value is None or isinstance(value, str):
23
+ return value
24
+ if value.tzinfo is None:
25
+ value = value.replace(tzinfo=timezone.utc)
26
+ return value.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
27
+
28
+
29
+ class Kavchat:
30
+ """Kavchat server client. Keep the secret key on your backend only.
31
+
32
+ >>> kavchat = Kavchat(os.environ["KAVCHAT_SECRET_KEY"])
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ secret_key: str,
38
+ *,
39
+ base_url: str = DEFAULT_BASE_URL,
40
+ timeout: float = 30.0,
41
+ locale: Optional[str] = None,
42
+ ) -> None:
43
+ if not isinstance(secret_key, str) or not secret_key:
44
+ raise ValueError("secret_key is required")
45
+ self._secret_key = secret_key
46
+ self.base_url = base_url.rstrip("/")
47
+ self.timeout = timeout
48
+ self.locale = locale
49
+
50
+ def __repr__(self) -> str: # never show the key
51
+ return f"Kavchat(base_url={self.base_url!r})"
52
+
53
+ # ------------------------------------------------------------ identity / webhooks
54
+
55
+ def create_user_token(
56
+ self,
57
+ user_id: str,
58
+ *,
59
+ phone: Optional[str] = None,
60
+ name: Optional[str] = None,
61
+ email: Optional[str] = None,
62
+ expires_in: int = DEFAULT_TOKEN_TTL_SECONDS,
63
+ issued_at: Optional[int] = None,
64
+ ) -> str:
65
+ """Sign an identity token for one of your users. See :func:`kavchat.create_user_token`."""
66
+ return create_user_token(
67
+ self._secret_key, user_id, phone=phone, name=name, email=email, expires_in=expires_in, issued_at=issued_at
68
+ )
69
+
70
+ def verify_webhook(
71
+ self,
72
+ raw_body: Union[str, bytes],
73
+ signature_header: Optional[str],
74
+ tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
75
+ *,
76
+ now: Optional[int] = None,
77
+ ) -> WebhookEvent:
78
+ """Verify a webhook request and return the event. See :func:`kavchat.verify_webhook`."""
79
+ return verify_webhook(self._secret_key, raw_body, signature_header, tolerance_seconds, now=now)
80
+
81
+ # ------------------------------------------------------------ Server API
82
+
83
+ def get_workspace(self) -> Workspace:
84
+ return cast(Workspace, self._request("GET", "/workspace"))
85
+
86
+ def list_tickets(
87
+ self, *, status: Optional[TicketStatus] = None, limit: Optional[int] = None, before: Optional[int] = None
88
+ ) -> List[Ticket]:
89
+ """Newest first. Page with ``before`` = the number of the last ticket you have."""
90
+ return cast(List[Ticket], self._request("GET", "/tickets", query={"status": status, "limit": limit, "before": before}))
91
+
92
+ def get_ticket(self, ticket_id: str) -> Ticket:
93
+ return cast(Ticket, self._request("GET", f"/tickets/{urllib.parse.quote(ticket_id, safe='')}"))
94
+
95
+ def set_ticket_status(self, ticket_id: str, status: TicketStatus) -> Ticket:
96
+ path = f"/tickets/{urllib.parse.quote(ticket_id, safe='')}/status"
97
+ return cast(Ticket, self._request("PUT", path, body={"status": status}))
98
+
99
+ def get_reports(self, *, from_: Optional[DateLike] = None, to: Optional[DateLike] = None) -> Report:
100
+ """Defaults to the last 30 days. ``from_`` has a trailing underscore because ``from`` is reserved."""
101
+ return cast(Report, self._request("GET", "/reports", query={"from": _iso(from_), "to": _iso(to)}))
102
+
103
+ def get_settings(self) -> Settings:
104
+ return cast(Settings, self._request("GET", "/settings"))
105
+
106
+ def update_settings(self, *, audience: Optional[Audience] = None, tester_phones: Optional[List[str]] = None) -> Settings:
107
+ """Only the audience and the tester phones can be changed from the server."""
108
+ body: Dict[str, Any] = {}
109
+ if audience is not None:
110
+ body["audience"] = audience
111
+ if tester_phones is not None:
112
+ body["testerPhones"] = list(tester_phones)
113
+ return cast(Settings, self._request("PUT", "/settings", body=body))
114
+
115
+ def list_releases(self) -> List[ReleaseSummary]:
116
+ return cast(List[ReleaseSummary], self._request("GET", "/releases"))
117
+
118
+ # ------------------------------------------------------------ transport
119
+
120
+ def _request(self, method: str, path: str, *, query: Optional[Dict[str, Any]] = None, body: Any = None) -> Any:
121
+ url = f"{self.base_url}/v1/server{path}"
122
+ params = {k: str(v) for k, v in (query or {}).items() if v is not None and v != ""}
123
+ if params:
124
+ url += "?" + urllib.parse.urlencode(params)
125
+ headers = {
126
+ "Authorization": f"Bearer {self._secret_key}",
127
+ "Accept": "application/json",
128
+ "User-Agent": f"kavchat-python/{VERSION}",
129
+ }
130
+ if self.locale:
131
+ headers["Accept-Language"] = self.locale
132
+ data = None
133
+ if body is not None:
134
+ data = json.dumps(body, ensure_ascii=False).encode("utf-8")
135
+ headers["Content-Type"] = "application/json"
136
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
137
+
138
+ try:
139
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
140
+ status, raw = res.status, res.read()
141
+ except urllib.error.HTTPError as err:
142
+ status, raw = err.code, err.read()
143
+ except (socket.timeout, TimeoutError):
144
+ raise KavchatApiError(0, "timeout", f"Request timed out after {self.timeout} s") from None
145
+ except urllib.error.URLError as err:
146
+ if isinstance(err.reason, (socket.timeout, TimeoutError)):
147
+ raise KavchatApiError(0, "timeout", f"Request timed out after {self.timeout} s") from None
148
+ raise KavchatApiError(0, "network_error", f"Could not reach Kavchat: {err.reason}") from None
149
+ except OSError as err:
150
+ raise KavchatApiError(0, "network_error", f"Could not reach Kavchat: {err}") from None
151
+
152
+ try:
153
+ payload = json.loads(raw.decode("utf-8")) if raw else None
154
+ except (UnicodeDecodeError, ValueError):
155
+ payload = None
156
+ if not isinstance(payload, dict) or not (200 <= status < 300) or payload.get("success") is not True:
157
+ if isinstance(payload, dict) and isinstance(payload.get("code"), str):
158
+ raise KavchatApiError(status, payload["code"], str(payload.get("message", payload["code"])), payload.get("details"))
159
+ raise KavchatApiError(status, "invalid_response", f"Unexpected response from Kavchat (HTTP {status})")
160
+ return payload.get("data")
kavchat/errors.py ADDED
@@ -0,0 +1,40 @@
1
+ """Errors raised by the Kavchat SDK."""
2
+ from typing import Any, Optional
3
+
4
+
5
+ class KavchatError(Exception):
6
+ """Base class of every error raised by this SDK."""
7
+
8
+
9
+ class KavchatApiError(KavchatError):
10
+ """The Server API answered with an error, or could not be reached.
11
+
12
+ ``status`` is the HTTP status, or 0 when no response arrived (``code`` is then
13
+ ``timeout`` or ``network_error``).
14
+ """
15
+
16
+ def __init__(self, status: int, code: str, message: str, details: Any = None) -> None:
17
+ super().__init__(message)
18
+ self.status = status
19
+ self.code = code
20
+ self.message = message
21
+ self.details = details
22
+
23
+ def __repr__(self) -> str:
24
+ return f"KavchatApiError(status={self.status!r}, code={self.code!r}, message={self.message!r})"
25
+
26
+
27
+ class WebhookVerificationError(KavchatError):
28
+ """A webhook request failed verification. Answer it with HTTP 400.
29
+
30
+ ``code`` is one of ``malformed_header``, ``timestamp_out_of_range``,
31
+ ``invalid_signature`` or ``invalid_payload``.
32
+ """
33
+
34
+ def __init__(self, code: str, message: str) -> None:
35
+ super().__init__(message)
36
+ self.code = code
37
+ self.message = message
38
+
39
+
40
+ __all__ = ["KavchatError", "KavchatApiError", "WebhookVerificationError"]
kavchat/identity.py ADDED
@@ -0,0 +1,62 @@
1
+ """Identity tokens: HS256 JWTs signed with the secret key, built with the standard library."""
2
+ import base64
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+ import time
7
+ from typing import Any, Dict, Optional
8
+
9
+ DEFAULT_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60
10
+ MAX_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60
11
+
12
+ # base64url of {"alg":"HS256","typ":"JWT"}
13
+ _HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
14
+
15
+
16
+ def _b64url(data: bytes) -> str:
17
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
18
+
19
+
20
+ def _utf16_length(value: str) -> int:
21
+ # The server counts characters as JavaScript does (UTF-16 code units).
22
+ return len(value.encode("utf-16-le")) // 2
23
+
24
+
25
+ def create_user_token(
26
+ secret_key: str,
27
+ user_id: str,
28
+ *,
29
+ phone: Optional[str] = None,
30
+ name: Optional[str] = None,
31
+ email: Optional[str] = None,
32
+ expires_in: int = DEFAULT_TOKEN_TTL_SECONDS,
33
+ issued_at: Optional[int] = None,
34
+ ) -> str:
35
+ """Sign an identity token for one of your users.
36
+
37
+ ``expires_in`` is in seconds (default 7 days, at most 30 days). ``issued_at`` is
38
+ unix seconds and defaults to now.
39
+ """
40
+ if not isinstance(secret_key, str) or not secret_key:
41
+ raise ValueError("secret_key is required")
42
+ if not isinstance(user_id, str) or not 1 <= _utf16_length(user_id) <= 128:
43
+ raise ValueError("user_id must be a string of 1 to 128 characters")
44
+ if isinstance(expires_in, bool) or not isinstance(expires_in, int) or not 0 < expires_in <= MAX_TOKEN_TTL_SECONDS:
45
+ raise ValueError(f"expires_in must be a whole number of seconds between 1 and {MAX_TOKEN_TTL_SECONDS}")
46
+ iat = int(time.time()) if issued_at is None else int(issued_at)
47
+
48
+ # Claim order is fixed (sub, phone, name, email, iat, exp) so every SDK signs the same bytes.
49
+ claims: Dict[str, Any] = {"sub": user_id}
50
+ if phone:
51
+ claims["phone"] = phone
52
+ if name:
53
+ claims["name"] = name
54
+ if email:
55
+ claims["email"] = email
56
+ claims["iat"] = iat
57
+ claims["exp"] = iat + expires_in
58
+
59
+ payload = _b64url(json.dumps(claims, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
60
+ signing_input = f"{_HEADER}.{payload}".encode("ascii")
61
+ signature = _b64url(hmac.new(secret_key.encode("utf-8"), signing_input, hashlib.sha256).digest())
62
+ return f"{_HEADER}.{payload}.{signature}"
kavchat/py.typed ADDED
File without changes
kavchat/types.py ADDED
@@ -0,0 +1,108 @@
1
+ """Typed shapes of Server API responses (plain dicts at runtime)."""
2
+ from typing import Any, Dict, List, Literal, Optional, TypedDict
3
+
4
+ TicketStatus = Literal["open", "closed"]
5
+ Audience = Literal["off", "testers", "all"]
6
+
7
+
8
+ # "from" is a Python keyword, so these two use the functional TypedDict syntax.
9
+ TranscriptEntry = TypedDict(
10
+ "TranscriptEntry",
11
+ {"from": Literal["bot", "user"], "text": str, "stepId": str, "at": str},
12
+ total=False,
13
+ )
14
+
15
+
16
+ class Ticket(TypedDict, total=False):
17
+ id: str
18
+ number: int
19
+ status: TicketStatus
20
+ name: Optional[str]
21
+ email: Optional[str]
22
+ phone: Optional[str]
23
+ userId: Optional[str]
24
+ topicId: Optional[str]
25
+ topicName: Optional[str]
26
+ scriptId: Optional[str]
27
+ sessionId: Optional[str]
28
+ message: str
29
+ transcript: List[TranscriptEntry]
30
+ createdAt: str
31
+ updatedAt: str
32
+
33
+
34
+ class ContactTopic(TypedDict):
35
+ id: str
36
+ name: str
37
+
38
+
39
+ class Settings(TypedDict, total=False):
40
+ enabled: bool
41
+ audience: Audience
42
+ testerPhones: List[str]
43
+ botName: str
44
+ avatarText: str
45
+ avatarImageUrl: Optional[str]
46
+ subtitle: str
47
+ primaryColor: str
48
+ position: Literal["start", "end"]
49
+ contactTopics: List[ContactTopic]
50
+
51
+
52
+ class Workspace(TypedDict, total=False):
53
+ id: str
54
+ name: str
55
+ locale: str
56
+ defaultCountry: str
57
+ plan: str
58
+ publicKey: str
59
+ secretKeyHint: str
60
+ webhookUrl: Optional[str]
61
+
62
+
63
+ class ReleaseScript(TypedDict, total=False):
64
+ id: str
65
+ kind: str
66
+ name: str
67
+
68
+
69
+ class ReleaseSummary(TypedDict, total=False):
70
+ version: int
71
+ note: str
72
+ restoredFrom: int
73
+ publishedAt: str
74
+ scripts: List[ReleaseScript]
75
+
76
+
77
+ class ReportTotals(TypedDict, total=False):
78
+ sessions: int
79
+ open: int
80
+ answered: int
81
+ contact: int
82
+ abandoned: int
83
+
84
+
85
+ class UnmatchedQuestion(TypedDict, total=False):
86
+ groupKey: str
87
+ sample: str
88
+ count: int
89
+ lastAt: str
90
+
91
+
92
+ Report = TypedDict(
93
+ "Report",
94
+ {
95
+ "from": str,
96
+ "to": str,
97
+ "totals": ReportTotals,
98
+ "byScript": List[Dict[str, Any]],
99
+ "dropOff": List[Dict[str, Any]],
100
+ "unmatched": List[UnmatchedQuestion],
101
+ },
102
+ total=False,
103
+ )
104
+
105
+
106
+ class WebhookEvent(TypedDict):
107
+ type: Literal["ticket.created"]
108
+ data: Ticket
kavchat/webhook.py ADDED
@@ -0,0 +1,58 @@
1
+ """Webhook verification for the ``Kavchat-Signature`` header."""
2
+ import hashlib
3
+ import hmac
4
+ import json
5
+ import time
6
+ from typing import List, Optional, Union, cast
7
+
8
+ from .errors import WebhookVerificationError
9
+ from .types import WebhookEvent
10
+
11
+ SIGNATURE_HEADER = "Kavchat-Signature"
12
+ DEFAULT_TOLERANCE_SECONDS = 300
13
+
14
+
15
+ def verify_webhook(
16
+ secret_key: str,
17
+ raw_body: Union[str, bytes],
18
+ signature_header: Optional[str],
19
+ tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
20
+ *,
21
+ now: Optional[int] = None,
22
+ ) -> WebhookEvent:
23
+ """Check the signature of a webhook request and return the parsed event.
24
+
25
+ ``raw_body`` must be the exact bytes received, before any JSON parsing.
26
+ Raises :class:`WebhookVerificationError` when the request is not authentic or too old.
27
+ """
28
+ timestamp: Optional[int] = None
29
+ bad_timestamp = False
30
+ signatures: List[str] = []
31
+ for part in (signature_header or "").split(","):
32
+ key, sep, value = part.partition("=")
33
+ if not sep:
34
+ continue
35
+ key, value = key.strip(), value.strip()
36
+ if key == "t":
37
+ if value.isascii() and value.isdigit():
38
+ timestamp = int(value)
39
+ else:
40
+ bad_timestamp = True
41
+ elif key == "v1" and value:
42
+ signatures.append(value)
43
+ if timestamp is None or bad_timestamp or not signatures:
44
+ raise WebhookVerificationError("malformed_header", "Kavchat-Signature header is missing or malformed")
45
+
46
+ current = int(time.time()) if now is None else int(now)
47
+ if abs(current - timestamp) > tolerance_seconds:
48
+ raise WebhookVerificationError("timestamp_out_of_range", "Webhook timestamp is outside the tolerance")
49
+
50
+ body = raw_body.encode("utf-8") if isinstance(raw_body, str) else bytes(raw_body)
51
+ expected = hmac.new(secret_key.encode("utf-8"), f"{timestamp}.".encode("ascii") + body, hashlib.sha256).hexdigest()
52
+ if not any(hmac.compare_digest(expected.encode("ascii"), sig.encode("utf-8")) for sig in signatures):
53
+ raise WebhookVerificationError("invalid_signature", "Webhook signature does not match")
54
+
55
+ try:
56
+ return cast(WebhookEvent, json.loads(body.decode("utf-8")))
57
+ except (UnicodeDecodeError, ValueError):
58
+ raise WebhookVerificationError("invalid_payload", "Webhook body is not valid JSON") from None
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: kavchat
3
+ Version: 0.0.4
4
+ Summary: Kavchat server SDK for Python: identity tokens, webhook verification and the Server API
5
+ Author-email: maxsave <support@kavchat.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://kavchat.com
8
+ Project-URL: Documentation, https://kavchat.com/docs/
9
+ Keywords: kavchat,chatbot,support,webhook,jwt
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Communications :: Chat
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # kavchat (Python)
23
+
24
+ Kavchat server SDK for Python 3.8+. Standard library only (`urllib`, `hmac`, `hashlib`, `json`), fully type hinted.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ pip install kavchat
30
+ ```
31
+
32
+ ## Setup
33
+
34
+ ```python
35
+ import os
36
+ from kavchat import Kavchat
37
+
38
+ kavchat = Kavchat(
39
+ os.environ["KAVCHAT_SECRET_KEY"],
40
+ # base_url="https://api.kavchat.com",
41
+ # timeout=30.0,
42
+ # locale="en", # language of error messages
43
+ )
44
+ ```
45
+
46
+ ## Identify your users
47
+
48
+ ```python
49
+ token = kavchat.create_user_token(
50
+ "user_123", # required, 1 to 128 characters
51
+ phone="+972501234567",
52
+ name="Dana",
53
+ email="dana@example.com",
54
+ expires_in=7 * 24 * 3600, # seconds, default 7 days, at most 30 days
55
+ )
56
+ ```
57
+
58
+ `kavchat.create_user_token(secret_key, user_id, ...)` also exists as a module function.
59
+
60
+ ## Verify webhooks
61
+
62
+ ```python
63
+ from flask import Flask, request
64
+ from kavchat import WebhookVerificationError
65
+
66
+ app = Flask(__name__)
67
+
68
+ @app.post("/kavchat/webhook")
69
+ def webhook():
70
+ try:
71
+ event = kavchat.verify_webhook(request.get_data(), request.headers.get("Kavchat-Signature"))
72
+ except WebhookVerificationError as err:
73
+ return err.code, 400
74
+ if event["type"] == "ticket.created":
75
+ print("New ticket", event["data"]["number"])
76
+ return "", 200
77
+ ```
78
+
79
+ Pass the raw bytes (`request.get_data()`, or `request.body` in Django). The optional third argument is the tolerance in seconds (default 300).
80
+
81
+ ## Server API
82
+
83
+ Responses are typed dicts (`kavchat.types`).
84
+
85
+ ```python
86
+ kavchat.get_workspace()
87
+ kavchat.list_tickets(status="open", limit=50, before=120)
88
+ kavchat.get_ticket(ticket_id)
89
+ kavchat.set_ticket_status(ticket_id, "closed")
90
+ kavchat.get_reports(from_=datetime(2026, 1, 1, tzinfo=timezone.utc), to=datetime.now(timezone.utc))
91
+ kavchat.get_settings()
92
+ kavchat.update_settings(audience="testers", tester_phones=["+972501234567"])
93
+ kavchat.list_releases()
94
+ ```
95
+
96
+ Errors raise `KavchatApiError` with `status`, `code`, `message` and `details`. `status` is 0 for `timeout` and `network_error`.
97
+
98
+ ## Tests
99
+
100
+ ```sh
101
+ PYTHONPATH=src python3 -m unittest discover -s tests -v
102
+ ```
103
+
104
+ ## License and support
105
+
106
+ MIT. Documentation: https://kavchat.com/docs/. Questions: support@kavchat.com.
@@ -0,0 +1,12 @@
1
+ kavchat/__init__.py,sha256=-5PVWw85vRKKGtdwx0NthD-F_bVLPEoWMzCj1Rr4W50,643
2
+ kavchat/client.py,sha256=Au4Y2Wsm1q5iDyZNts4lWpRZBBwhBzvWU3q6fA18Uoc,7151
3
+ kavchat/errors.py,sha256=HnxAvElU9yf6N45FgP6w4EcUv5DCTMutZJa9CoHviic,1256
4
+ kavchat/identity.py,sha256=5s564Y21o0gb5SVR2vFA0cbDQaqNFF2ToXL7Y3QUzGI,2311
5
+ kavchat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ kavchat/types.py,sha256=I9dEp1URmLtkLI5Kotxh_PMItGSPQ7LSYI8qzApFi3Y,2246
7
+ kavchat/webhook.py,sha256=4A_NCxbVQY2pwsvHPD-40ZKG3g2rQvp1AeDBUIBgqoc,2346
8
+ kavchat-0.0.4.dist-info/licenses/LICENSE,sha256=ywlod2YvbvK17sQVt7mnKduK_kgzjq5owYKBSNyovt8,1064
9
+ kavchat-0.0.4.dist-info/METADATA,sha256=eDiplFW2E5pZGCwyLDQalyD1vfreaH6nsF7Pma5oaSQ,2944
10
+ kavchat-0.0.4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
11
+ kavchat-0.0.4.dist-info/top_level.txt,sha256=ewth_geCmq04KBJ8__Xw6Pb3m8jDGEyGihppzMI-mG8,8
12
+ kavchat-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 maxsave
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ kavchat