tgwidget 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.
- tgwidget/__init__.py +5 -0
- tgwidget/builder.py +113 -0
- tgwidget/parser.py +127 -0
- tgwidget/types.py +14 -0
- tgwidget-0.1.0.dist-info/METADATA +94 -0
- tgwidget-0.1.0.dist-info/RECORD +7 -0
- tgwidget-0.1.0.dist-info/WHEEL +4 -0
tgwidget/__init__.py
ADDED
tgwidget/builder.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from base64 import b64encode
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
VALID_COLOR_FORMATS,
|
|
10
|
+
VALID_COLOR_SCHEMES,
|
|
11
|
+
VALID_DATE_FORMATS,
|
|
12
|
+
VALID_DATE_MODES,
|
|
13
|
+
VALID_DATE_ORDERS,
|
|
14
|
+
ColorFormat,
|
|
15
|
+
ColorScheme,
|
|
16
|
+
DateFormat,
|
|
17
|
+
DateMode,
|
|
18
|
+
DateOrder,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
BASE_URL = "https://tgwidget.github.io/"
|
|
22
|
+
|
|
23
|
+
_HEX_RE = re.compile(r"^#?[0-9A-Fa-f]{6}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_hex(color: str, name: str) -> str:
|
|
27
|
+
if not _HEX_RE.match(color):
|
|
28
|
+
raise ValueError(f"{name} must be a valid hex color (e.g. '#FF0000'), got '{color}'")
|
|
29
|
+
return color if color.startswith("#") else f"#{color}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TgWidget:
|
|
33
|
+
"""Builder for TeleWidget URLs."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, bot_username: str) -> None:
|
|
36
|
+
if not bot_username:
|
|
37
|
+
raise ValueError("bot_username is required")
|
|
38
|
+
self._bot_username = bot_username
|
|
39
|
+
self._widget: Optional[str] = None
|
|
40
|
+
self._payload: dict[str, Any] = {}
|
|
41
|
+
self._style: dict[str, Any] = {}
|
|
42
|
+
|
|
43
|
+
def date(
|
|
44
|
+
self,
|
|
45
|
+
mode: DateMode = "date",
|
|
46
|
+
format: DateFormat = "default",
|
|
47
|
+
order: DateOrder = "ymd",
|
|
48
|
+
) -> TgWidget:
|
|
49
|
+
if mode not in VALID_DATE_MODES:
|
|
50
|
+
raise ValueError(f"Invalid date mode '{mode}'. Must be one of: {VALID_DATE_MODES}")
|
|
51
|
+
if format not in VALID_DATE_FORMATS:
|
|
52
|
+
raise ValueError(f"Invalid date format '{format}'. Must be one of: {VALID_DATE_FORMATS}")
|
|
53
|
+
if order not in VALID_DATE_ORDERS:
|
|
54
|
+
raise ValueError(f"Invalid date order '{order}'. Must be one of: {VALID_DATE_ORDERS}")
|
|
55
|
+
self._widget = "date"
|
|
56
|
+
self._payload = {"mode": mode, "format": format, "order": order}
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def color(self, format: ColorFormat = "hex") -> TgWidget:
|
|
60
|
+
if format not in VALID_COLOR_FORMATS:
|
|
61
|
+
raise ValueError(f"Invalid color format '{format}'. Must be one of: {VALID_COLOR_FORMATS}")
|
|
62
|
+
self._widget = "color"
|
|
63
|
+
self._payload = {"format": format}
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def schedule(self) -> TgWidget:
|
|
67
|
+
self._widget = "schedule"
|
|
68
|
+
self._payload = {"format": "bunch"}
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def style(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
color_scheme: ColorScheme = "light",
|
|
75
|
+
accent: Optional[str] = None,
|
|
76
|
+
tint: Optional[str] = None,
|
|
77
|
+
liquid_glass: bool = False,
|
|
78
|
+
adapt_tg_theme: bool = False,
|
|
79
|
+
adopt_tg_palette: bool = False,
|
|
80
|
+
) -> TgWidget:
|
|
81
|
+
if color_scheme not in VALID_COLOR_SCHEMES:
|
|
82
|
+
raise ValueError(f"Invalid color_scheme '{color_scheme}'. Must be one of: {VALID_COLOR_SCHEMES}")
|
|
83
|
+
self._style = {
|
|
84
|
+
"colorScheme": color_scheme,
|
|
85
|
+
"liquidGlass": liquid_glass,
|
|
86
|
+
"adaptTgTheme": adapt_tg_theme,
|
|
87
|
+
"adoptTgPalette": adopt_tg_palette,
|
|
88
|
+
}
|
|
89
|
+
if accent:
|
|
90
|
+
self._style["accent"] = _validate_hex(accent, "accent")
|
|
91
|
+
if tint:
|
|
92
|
+
self._style["tint"] = _validate_hex(tint, "tint")
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def _build_payload(self) -> dict[str, Any]:
|
|
96
|
+
if not self._widget:
|
|
97
|
+
raise ValueError("No widget type set. Call .date(), .color(), or .schedule() first.")
|
|
98
|
+
payload: dict[str, Any] = {
|
|
99
|
+
"widget": self._widget,
|
|
100
|
+
"bot_username": self._bot_username,
|
|
101
|
+
**self._payload,
|
|
102
|
+
}
|
|
103
|
+
if self._style:
|
|
104
|
+
payload["style"] = self._style
|
|
105
|
+
return payload
|
|
106
|
+
|
|
107
|
+
def url(self, base_url: str = BASE_URL) -> str:
|
|
108
|
+
payload = self._build_payload()
|
|
109
|
+
encoded = b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode()
|
|
110
|
+
return f"{base_url}?p={encoded}"
|
|
111
|
+
|
|
112
|
+
def payload(self) -> dict[str, Any]:
|
|
113
|
+
return self._build_payload()
|
tgwidget/parser.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .types import ColorFormat, DateFormat, DateMode, DateOrder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DateResult:
|
|
12
|
+
date: Optional[str] = None
|
|
13
|
+
time: Optional[str] = None
|
|
14
|
+
date_end: Optional[str] = None
|
|
15
|
+
time_end: Optional[str] = None
|
|
16
|
+
timestamp: Optional[int] = None
|
|
17
|
+
timestamp_end: Optional[int] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ColorResult:
|
|
22
|
+
raw: str
|
|
23
|
+
hex: Optional[str] = None
|
|
24
|
+
rgb: Optional[tuple[int, int, int]] = None
|
|
25
|
+
hsl: Optional[tuple[int, int, int]] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ScheduleDay:
|
|
30
|
+
enabled: bool
|
|
31
|
+
start: Optional[str] = None
|
|
32
|
+
end: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ScheduleResult:
|
|
37
|
+
days: list[ScheduleDay]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_date(
|
|
41
|
+
value: str,
|
|
42
|
+
mode: DateMode = "date",
|
|
43
|
+
format: DateFormat = "default",
|
|
44
|
+
order: DateOrder = "ymd",
|
|
45
|
+
) -> DateResult:
|
|
46
|
+
"""Parse date widget result string back into structured data."""
|
|
47
|
+
result = DateResult()
|
|
48
|
+
|
|
49
|
+
if format in ("unix-s", "unix-ms"):
|
|
50
|
+
parts = value.split("_")
|
|
51
|
+
div = 1000 if format == "unix-s" else 1
|
|
52
|
+
ts = int(parts[0]) * div if format == "unix-s" else int(parts[0])
|
|
53
|
+
result.timestamp = int(parts[0])
|
|
54
|
+
if len(parts) > 1:
|
|
55
|
+
result.timestamp_end = int(parts[1])
|
|
56
|
+
dt = datetime.fromtimestamp(ts / 1000)
|
|
57
|
+
result.date = dt.strftime("%Y-%m-%d")
|
|
58
|
+
result.time = dt.strftime("%H:%M")
|
|
59
|
+
if result.timestamp_end is not None:
|
|
60
|
+
ts_end = int(parts[1]) * div if format == "unix-s" else int(parts[1])
|
|
61
|
+
dt_end = datetime.fromtimestamp(ts_end / 1000)
|
|
62
|
+
result.date_end = dt_end.strftime("%Y-%m-%d")
|
|
63
|
+
result.time_end = dt_end.strftime("%H:%M")
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
if mode == "date":
|
|
67
|
+
result.date = _parse_date_str(value, order)
|
|
68
|
+
elif mode == "time":
|
|
69
|
+
h, m = value.split("-")
|
|
70
|
+
result.time = f"{h}:{m}"
|
|
71
|
+
elif mode == "datetime":
|
|
72
|
+
date_part, time_part = value.split("_")
|
|
73
|
+
result.date = _parse_date_str(date_part, order)
|
|
74
|
+
h, m = time_part.split("-")
|
|
75
|
+
result.time = f"{h}:{m}"
|
|
76
|
+
elif mode == "date-range":
|
|
77
|
+
parts = value.split("_")
|
|
78
|
+
result.date = _parse_date_str(parts[0], order)
|
|
79
|
+
result.date_end = _parse_date_str(parts[1], order)
|
|
80
|
+
elif mode == "time-range":
|
|
81
|
+
parts = value.split("_")
|
|
82
|
+
h1, m1 = parts[0].split("-")
|
|
83
|
+
h2, m2 = parts[1].split("-")
|
|
84
|
+
result.time = f"{h1}:{m1}"
|
|
85
|
+
result.time_end = f"{h2}:{m2}"
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_date_str(value: str, order: DateOrder) -> str:
|
|
91
|
+
parts = value.split("-")
|
|
92
|
+
if order == "ymd":
|
|
93
|
+
return f"{parts[0]}-{parts[1]}-{parts[2]}"
|
|
94
|
+
elif order == "dmy":
|
|
95
|
+
return f"{parts[2]}-{parts[1]}-{parts[0]}"
|
|
96
|
+
else: # mdy
|
|
97
|
+
return f"{parts[2]}-{parts[0]}-{parts[1]}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_color(value: str, format: ColorFormat = "hex") -> ColorResult:
|
|
101
|
+
"""Parse color widget result string."""
|
|
102
|
+
result = ColorResult(raw=value)
|
|
103
|
+
if format == "hex":
|
|
104
|
+
result.hex = f"#{value}"
|
|
105
|
+
elif format == "rgb":
|
|
106
|
+
parts = value.split("_")
|
|
107
|
+
result.rgb = (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
108
|
+
elif format == "hsl":
|
|
109
|
+
parts = value.split("_")
|
|
110
|
+
result.hsl = (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_schedule(value: str) -> ScheduleResult:
|
|
115
|
+
"""Parse schedule widget result (bunch format: 56 chars, 8 per day)."""
|
|
116
|
+
if len(value) != 56:
|
|
117
|
+
raise ValueError(f"Schedule bunch format must be 56 chars, got {len(value)}")
|
|
118
|
+
days: list[ScheduleDay] = []
|
|
119
|
+
for i in range(7):
|
|
120
|
+
chunk = value[i * 8 : (i + 1) * 8]
|
|
121
|
+
if chunk == "00000000":
|
|
122
|
+
days.append(ScheduleDay(enabled=False))
|
|
123
|
+
else:
|
|
124
|
+
start = f"{chunk[0:2]}:{chunk[2:4]}"
|
|
125
|
+
end = f"{chunk[4:6]}:{chunk[6:8]}"
|
|
126
|
+
days.append(ScheduleDay(enabled=True, start=start, end=end))
|
|
127
|
+
return days
|
tgwidget/types.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
DateMode = Literal["date", "time", "datetime", "date-range", "time-range"]
|
|
4
|
+
DateFormat = Literal["default", "unix-s", "unix-ms"]
|
|
5
|
+
DateOrder = Literal["ymd", "dmy", "mdy"]
|
|
6
|
+
ColorFormat = Literal["hex", "rgb", "hsl"]
|
|
7
|
+
ColorScheme = Literal["light", "dark", "auto"]
|
|
8
|
+
ScheduleFormat = Literal["bunch"]
|
|
9
|
+
|
|
10
|
+
VALID_DATE_MODES = {"date", "time", "datetime", "date-range", "time-range"}
|
|
11
|
+
VALID_DATE_FORMATS = {"default", "unix-s", "unix-ms"}
|
|
12
|
+
VALID_DATE_ORDERS = {"ymd", "dmy", "mdy"}
|
|
13
|
+
VALID_COLOR_FORMATS = {"hex", "rgb", "hsl"}
|
|
14
|
+
VALID_COLOR_SCHEMES = {"light", "dark", "auto"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tgwidget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for TeleWidget — Telegram Mini App widgets
|
|
5
|
+
Project-URL: Homepage, https://github.com/tgwidget/tgwidget-python
|
|
6
|
+
Project-URL: Repository, https://github.com/tgwidget/tgwidget-python
|
|
7
|
+
Author: tgwidget
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: bot,mini-apps,telegram,widget
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Communications :: Chat
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tgwidget
|
|
19
|
+
|
|
20
|
+
Python SDK for [TeleWidget](https://tgwidget.github.io/) — beautiful Telegram Mini App widgets for bots.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install tgwidget
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Generate widget URL
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from tgwidget import TgWidget
|
|
34
|
+
|
|
35
|
+
# Date picker
|
|
36
|
+
url = TgWidget("your_bot").date(mode="datetime", format="unix-s").url()
|
|
37
|
+
|
|
38
|
+
# Color picker
|
|
39
|
+
url = TgWidget("your_bot").color(format="hex").url()
|
|
40
|
+
|
|
41
|
+
# Schedule
|
|
42
|
+
url = TgWidget("your_bot").schedule().url()
|
|
43
|
+
|
|
44
|
+
# With styling
|
|
45
|
+
url = (
|
|
46
|
+
TgWidget("your_bot")
|
|
47
|
+
.date(mode="date")
|
|
48
|
+
.style(color_scheme="dark", accent="#FF6600", adopt_tg_palette=True)
|
|
49
|
+
.url()
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Parse results
|
|
54
|
+
|
|
55
|
+
When a user completes the widget, the result comes back via deep link `t.me/your_bot?start=VALUE`. Parse the value:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from tgwidget import parse_date, parse_color, parse_schedule
|
|
59
|
+
|
|
60
|
+
# Date result
|
|
61
|
+
result = parse_date("2025-03-15", mode="date")
|
|
62
|
+
# DateResult(date='2025-03-15', time=None, ...)
|
|
63
|
+
|
|
64
|
+
# Date range with unix timestamps
|
|
65
|
+
result = parse_date("1710460800_1718236800", mode="date-range", format="unix-s")
|
|
66
|
+
# DateResult(timestamp=1710460800, timestamp_end=1718236800, date='2025-03-15', ...)
|
|
67
|
+
|
|
68
|
+
# Color result
|
|
69
|
+
result = parse_color("FF6600", format="hex")
|
|
70
|
+
# ColorResult(raw='FF6600', hex='#FF6600')
|
|
71
|
+
|
|
72
|
+
# Schedule result (56-char bunch format)
|
|
73
|
+
result = parse_schedule("09001800090018000000000009001800090018000000000000000000")
|
|
74
|
+
# [ScheduleDay(enabled=True, start='09:00', end='18:00'), ...]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `TgWidget(bot_username)`
|
|
80
|
+
|
|
81
|
+
Create a widget builder.
|
|
82
|
+
|
|
83
|
+
- `.date(mode, format, order)` — Date/time picker
|
|
84
|
+
- `.color(format)` — Color picker
|
|
85
|
+
- `.schedule()` — Weekly schedule
|
|
86
|
+
- `.style(color_scheme, accent, tint, liquid_glass, adapt_tg_theme, adopt_tg_palette)` — Styling
|
|
87
|
+
- `.url(base_url)` — Generate the final URL
|
|
88
|
+
- `.payload()` — Get the raw payload dict
|
|
89
|
+
|
|
90
|
+
### Parsers
|
|
91
|
+
|
|
92
|
+
- `parse_date(value, mode, format, order)` → `DateResult`
|
|
93
|
+
- `parse_color(value, format)` → `ColorResult`
|
|
94
|
+
- `parse_schedule(value)` → `list[ScheduleDay]`
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tgwidget/__init__.py,sha256=ETyxnugOQSsxCyQ5fgEngBjWxMixQDo2hpSvZ6ouZd8,183
|
|
2
|
+
tgwidget/builder.py,sha256=QVN2_pSk-6UcsSg9ha20DzuSBP6XNMgm2qUzs77DvsU,3753
|
|
3
|
+
tgwidget/parser.py,sha256=6ge6Zhi50xzk8Aopgo3h3ZJxdYhJhz-hBiQwgJaARl8,3980
|
|
4
|
+
tgwidget/types.py,sha256=IE7oF1Qt1w2-k5PSPB8hD6stxqw5baT-5dc1iXTqAig,586
|
|
5
|
+
tgwidget-0.1.0.dist-info/METADATA,sha256=orT3CYbS09gmLiw3j1itR3gbrP2zTJyY-OZoXNrPkgM,2610
|
|
6
|
+
tgwidget-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
tgwidget-0.1.0.dist-info/RECORD,,
|