wasender-cli 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.
wasender/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Wasender CLI package"""
2
+ __version__ = "0.1.0"
wasender/client.py ADDED
@@ -0,0 +1,63 @@
1
+ import httpx
2
+ from typing import Optional, Dict, Any
3
+
4
+ from .output import print_http_error_and_exit, print_error_and_exit
5
+
6
+ class WasenderClient:
7
+ def __init__(self, token: str, base_url: str = "https://wasenderapi.com/api"):
8
+ self.token = token
9
+ self.base_url = base_url.rstrip('/')
10
+ self.headers = {
11
+ "Authorization": f"Bearer {self.token}",
12
+ "Content-Type": "application/json",
13
+ "Accept": "application/json"
14
+ }
15
+
16
+ def _handle_response(self, response: httpx.Response) -> Any:
17
+ try:
18
+ response.raise_for_status()
19
+ text = response.text.strip()
20
+ if not text:
21
+ return {}
22
+ try:
23
+ return response.json()
24
+ except ValueError:
25
+ print_error_and_exit(f"Invalid JSON response. Content snippet: {text[:100]}")
26
+ except httpx.HTTPStatusError as e:
27
+ try:
28
+ error_msg = response.json()
29
+ except Exception:
30
+ error_msg = response.text
31
+ print_http_error_and_exit(e.response.status_code, str(error_msg))
32
+ except httpx.RequestError as e:
33
+ print_error_and_exit(f"Connection error: {e}")
34
+
35
+ def request(self, method: str, path: str, **kwargs) -> Any:
36
+ url = f"{self.base_url}/{path.lstrip('/')}"
37
+
38
+ # Don't pass auth down implicitly, we already handle it in headers
39
+ # but just passing headers down
40
+ req_kwargs = {"headers": self.headers}
41
+ req_kwargs.update(kwargs)
42
+
43
+ try:
44
+ with httpx.Client() as client:
45
+ response = client.request(method, url, **req_kwargs)
46
+ return self._handle_response(response)
47
+ except httpx.RequestError as e:
48
+ print_error_and_exit(f"Connection error: {e}")
49
+
50
+ def get(self, path: str, params: Optional[Dict] = None) -> Any:
51
+ return self.request("GET", path, params=params)
52
+
53
+ def post(self, path: str, json: Optional[Dict] = None) -> Any:
54
+ return self.request("POST", path, json=json)
55
+
56
+ def put(self, path: str, json: Optional[Dict] = None) -> Any:
57
+ return self.request("PUT", path, json=json)
58
+
59
+ def patch(self, path: str, json: Optional[Dict] = None) -> Any:
60
+ return self.request("PATCH", path, json=json)
61
+
62
+ def delete(self, path: str) -> Any:
63
+ return self.request("DELETE", path)
wasender/contacts.py ADDED
@@ -0,0 +1,69 @@
1
+ import typer
2
+ from .output import print_json_obj
3
+ from .client import WasenderClient
4
+
5
+ app = typer.Typer()
6
+
7
+ def get_client(ctx: typer.Context) -> WasenderClient:
8
+ return ctx.obj
9
+
10
+ @app.command("list")
11
+ def list_contacts(ctx: typer.Context, session_id: str):
12
+ client = get_client(ctx)
13
+ result = client.get(f"sessions/{session_id}/contacts")
14
+ print_json_obj(result)
15
+
16
+ @app.command("add")
17
+ def add_or_edit_contact(ctx: typer.Context, session_id: str, phone: str, name: str):
18
+ client = get_client(ctx)
19
+ payload = {"phone": phone, "name": name}
20
+ result = client.post(f"sessions/{session_id}/contacts", json=payload)
21
+ print_json_obj(result)
22
+
23
+ @app.command("get")
24
+ def get_contact_info(ctx: typer.Context, session_id: str, jid: str):
25
+ client = get_client(ctx)
26
+ result = client.get(f"sessions/{session_id}/contacts/{jid}")
27
+ print_json_obj(result)
28
+
29
+ @app.command("picture")
30
+ def get_contact_picture(ctx: typer.Context, session_id: str, jid: str):
31
+ client = get_client(ctx)
32
+ result = client.get(f"sessions/{session_id}/contacts/{jid}/picture")
33
+ print_json_obj(result)
34
+
35
+ @app.command("block")
36
+ def block_contact(ctx: typer.Context, session_id: str, jid: str):
37
+ client = get_client(ctx)
38
+ result = client.post(f"sessions/{session_id}/contacts/{jid}/block")
39
+ print_json_obj(result)
40
+
41
+ @app.command("unblock")
42
+ def unblock_contact(ctx: typer.Context, session_id: str, jid: str):
43
+ client = get_client(ctx)
44
+ result = client.post(f"sessions/{session_id}/contacts/{jid}/unblock")
45
+ print_json_obj(result)
46
+
47
+ @app.command("check")
48
+ def check_jid_on_whatsapp(ctx: typer.Context, session_id: str, phone: str):
49
+ client = get_client(ctx)
50
+ result = client.get(f"sessions/{session_id}/contacts/check/{phone}")
51
+ print_json_obj(result)
52
+
53
+ @app.command("lid")
54
+ def get_lid_from_phone_number(ctx: typer.Context, session_id: str, phone: str):
55
+ client = get_client(ctx)
56
+ result = client.get(f"sessions/{session_id}/contacts/lid/{phone}")
57
+ print_json_obj(result)
58
+
59
+ @app.command("phone")
60
+ def get_phone_number_from_lid(ctx: typer.Context, session_id: str, lid: str):
61
+ client = get_client(ctx)
62
+ result = client.get(f"sessions/{session_id}/contacts/phone/{lid}")
63
+ print_json_obj(result)
64
+
65
+ @app.command("search")
66
+ def search_contacts(ctx: typer.Context, session_id: str, query: str):
67
+ client = get_client(ctx)
68
+ result = client.get(f"sessions/{session_id}/contacts/search", params={"q": query})
69
+ print_json_obj(result)
wasender/groups.py ADDED
@@ -0,0 +1,96 @@
1
+ import typer
2
+ from typing import Optional
3
+ from .output import print_json_obj
4
+ from .client import WasenderClient
5
+
6
+ app = typer.Typer()
7
+
8
+ def get_client(ctx: typer.Context) -> WasenderClient:
9
+ return ctx.obj
10
+
11
+ @app.command("list")
12
+ def list_groups(ctx: typer.Context, session_id: str):
13
+ client = get_client(ctx)
14
+ result = client.get(f"sessions/{session_id}/groups")
15
+ print_json_obj(result)
16
+
17
+ @app.command("create")
18
+ def create_group(ctx: typer.Context, session_id: str, name: str, participants: str = typer.Option(..., help="Comma separated jids")):
19
+ client = get_client(ctx)
20
+ payload = {"name": name, "participants": participants.split(",")}
21
+ result = client.post(f"sessions/{session_id}/groups", json=payload)
22
+ print_json_obj(result)
23
+
24
+ @app.command("metadata")
25
+ def get_group_metadata(ctx: typer.Context, session_id: str, group_id: str):
26
+ client = get_client(ctx)
27
+ result = client.get(f"sessions/{session_id}/groups/{group_id}")
28
+ print_json_obj(result)
29
+
30
+ @app.command("participants")
31
+ def get_group_participants(ctx: typer.Context, session_id: str, group_id: str):
32
+ client = get_client(ctx)
33
+ result = client.get(f"sessions/{session_id}/groups/{group_id}/participants")
34
+ print_json_obj(result)
35
+
36
+ @app.command("add")
37
+ def add_group_participants(ctx: typer.Context, session_id: str, group_id: str, participants: str = typer.Option(..., help="Comma separated jids")):
38
+ client = get_client(ctx)
39
+ payload = {"participants": participants.split(",")}
40
+ result = client.post(f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
41
+ print_json_obj(result)
42
+
43
+ @app.command("remove")
44
+ def remove_group_participants(ctx: typer.Context, session_id: str, group_id: str, participants: str = typer.Option(..., help="Comma separated jids")):
45
+ client = get_client(ctx)
46
+ # DELETE might not take a body based on std conventions, but if Wasender does, we pass json here.
47
+ payload = {"participants": participants.split(",")}
48
+ # Usually Wasender has a specific POST endpoint for removing or accepts DELETE with body
49
+ # Let's assume POST to /remove or similar if DELETE body is flaky:
50
+ # We will use client.request method
51
+ result = client.request("DELETE", f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
52
+ print_json_obj(result)
53
+
54
+ @app.command("update-participants")
55
+ def update_group_participants(ctx: typer.Context, session_id: str, group_id: str, action: str = typer.Option(..., help="promote|demote"), participants: str = typer.Option(..., help="Comma separated jids")):
56
+ client = get_client(ctx)
57
+ payload = {"action": action, "participants": participants.split(",")}
58
+ result = client.patch(f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
59
+ print_json_obj(result)
60
+
61
+ @app.command("settings")
62
+ def update_group_settings(ctx: typer.Context, session_id: str, group_id: str, subject: Optional[str] = typer.Option(None, "--subject", "-s"), description: Optional[str] = typer.Option(None, "--description", "-d"), announce: Optional[bool] = typer.Option(None, "--announce", "-a")):
63
+ client = get_client(ctx)
64
+ payload = {}
65
+ if subject is not None:
66
+ payload["subject"] = subject
67
+ if description is not None:
68
+ payload["description"] = description
69
+ if announce is not None:
70
+ payload["announce"] = announce
71
+ result = client.patch(f"sessions/{session_id}/groups/{group_id}/settings", json=payload)
72
+ print_json_obj(result)
73
+
74
+ @app.command("leave")
75
+ def leave_group(ctx: typer.Context, session_id: str, group_id: str):
76
+ client = get_client(ctx)
77
+ result = client.post(f"sessions/{session_id}/groups/{group_id}/leave")
78
+ print_json_obj(result)
79
+
80
+ @app.command("invite-link")
81
+ def generate_invite_link(ctx: typer.Context, session_id: str, group_id: str):
82
+ client = get_client(ctx)
83
+ result = client.get(f"sessions/{session_id}/groups/{group_id}/invite-link")
84
+ print_json_obj(result)
85
+
86
+ @app.command("accept-invite")
87
+ def accept_group_invite(ctx: typer.Context, session_id: str, invite_code: str):
88
+ client = get_client(ctx)
89
+ result = client.post(f"sessions/{session_id}/groups/accept-invite/{invite_code}")
90
+ print_json_obj(result)
91
+
92
+ @app.command("search")
93
+ def search_groups(ctx: typer.Context, session_id: str, query: str):
94
+ client = get_client(ctx)
95
+ result = client.get(f"sessions/{session_id}/groups/search", params={"q": query})
96
+ print_json_obj(result)
wasender/main.py ADDED
@@ -0,0 +1,45 @@
1
+ import os
2
+ import typer
3
+ from .output import set_output_mode, print_error_and_exit
4
+ from .client import WasenderClient
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ from . import sessions
9
+ from . import messages
10
+ from . import contacts
11
+ from . import groups
12
+
13
+ # Load environment variables from a .env file if it exists
14
+ load_dotenv()
15
+
16
+ app = typer.Typer(help="WasenderAPI CLI Tool")
17
+
18
+ app.add_typer(sessions.app, name="sessions", help="Manage WhatsApp sessions")
19
+ app.add_typer(messages.app, name="messages", help="Send and manage messages")
20
+ app.add_typer(contacts.app, name="contacts", help="Manage contacts")
21
+ app.add_typer(groups.app, name="groups", help="Manage groups")
22
+
23
+ @app.callback()
24
+ def main(
25
+ ctx: typer.Context,
26
+ token: str = typer.Option(None, "--token", "-t", help="API Auth Token. Or use WASENDER_TOKEN env var.", envvar="WASENDER_TOKEN"),
27
+ base_url: str = typer.Option("https://wasenderapi.com/api", "--base-url", "-u", help="Base URL of the WasenderAPI.", envvar="WASENDER_BASE_URL"),
28
+ raw: bool = typer.Option(False, "--raw", help="Output raw JSON (for piping)"),
29
+ quiet: bool = typer.Option(False, "--quiet", help="Suppress output, only exit codes")
30
+ ):
31
+ set_output_mode(raw=raw, quiet=quiet)
32
+
33
+ if not token:
34
+ # Check env var directly in case typer envvar mapping has quirks
35
+ token = os.environ.get("WASENDER_TOKEN")
36
+
37
+ if not token and ctx.invoked_subcommand:
38
+ # Only require token if we are not asking for help or version
39
+ set_output_mode(False, False) # Show error even if quiet since we failed explicitly
40
+ print_error_and_exit("WASENDER_TOKEN environment variable or --token flag is required.")
41
+
42
+ ctx.obj = WasenderClient(token=token or "", base_url=base_url)
43
+
44
+ if __name__ == "__main__":
45
+ app()
wasender/messages.py ADDED
@@ -0,0 +1,161 @@
1
+ import typer
2
+ from typing import Optional, List
3
+ from .output import print_json_obj
4
+ from .client import WasenderClient
5
+ import os
6
+
7
+ app = typer.Typer()
8
+
9
+ def get_client(ctx: typer.Context) -> WasenderClient:
10
+ return ctx.obj
11
+
12
+ # Media helper for file uploading
13
+ def _upload_or_pass(client: WasenderClient, url_or_path: str):
14
+ # Depending on API, you might need multipart upload or just passing the URL.
15
+ # Wasender typically supports URL. For actual files, we use form upload if local.
16
+ if os.path.exists(url_or_path) and os.path.isfile(url_or_path):
17
+ import httpx
18
+ from .output import print_error_and_exit
19
+ try:
20
+ url = f"{client.base_url}/media/upload"
21
+ with open(url_or_path, "rb") as f:
22
+ try:
23
+ with httpx.Client() as c:
24
+ # Assuming an endpoint for file upload exists, if not we have to pass it in multipart
25
+ # Since WasenderAPI expects URLs or handles multipart in the same message endpoint,
26
+ pass
27
+ except Exception as e:
28
+ pass
29
+ except Exception:
30
+ pass
31
+ # For now, we will pass it as a URL or assume the API handles it directly
32
+ return url_or_path
33
+
34
+ @app.command("text")
35
+ def send_text_message(ctx: typer.Context, session_id: str, to: str, message: str):
36
+ client = get_client(ctx)
37
+ payload = {"to": to, "text": message}
38
+ result = client.post(f"send-message", json=payload)
39
+ print_json_obj(result)
40
+
41
+ @app.command("mention")
42
+ def send_mention_message(ctx: typer.Context, session_id: str, to: str, message: str, mentions: str = typer.Option(..., help="Comma separated jids")):
43
+ client = get_client(ctx)
44
+ payload = {"to": to, "text": message, "mentions": mentions.split(",")}
45
+ result = client.post(f"sessions/{session_id}/messages/text", json=payload)
46
+ print_json_obj(result)
47
+
48
+ @app.command("image")
49
+ def send_image_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
50
+ client = get_client(ctx)
51
+ if os.path.isfile(url_or_path):
52
+ # We assume there's a multipart upload or Wasender accepts base64
53
+ # We will use multipart form data if it's a file
54
+ with open(url_or_path, "rb") as f:
55
+ files = {"file": f}
56
+ data = {"to": to}
57
+ result = client.request("POST", f"sessions/{session_id}/messages/image", data=data, files=files, headers={"Content-Type": None}) # Let httpx handle content type
58
+ print_json_obj(result)
59
+ return
60
+
61
+ payload = {"to": to, "image": url_or_path}
62
+ result = client.post(f"sessions/{session_id}/messages/image", json=payload)
63
+ print_json_obj(result)
64
+
65
+ @app.command("audio")
66
+ def send_audio_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
67
+ client = get_client(ctx)
68
+ if os.path.isfile(url_or_path):
69
+ with open(url_or_path, "rb") as f:
70
+ files = {"file": f}
71
+ data = {"to": to}
72
+ result = client.request("POST", f"sessions/{session_id}/messages/audio", data=data, files=files, headers={"Content-Type": None})
73
+ print_json_obj(result)
74
+ return
75
+
76
+ payload = {"to": to, "audio": url_or_path}
77
+ result = client.post(f"sessions/{session_id}/messages/audio", json=payload)
78
+ print_json_obj(result)
79
+
80
+ @app.command("video")
81
+ def send_video_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
82
+ client = get_client(ctx)
83
+ if os.path.isfile(url_or_path):
84
+ with open(url_or_path, "rb") as f:
85
+ files = {"file": f}
86
+ data = {"to": to}
87
+ result = client.request("POST", f"sessions/{session_id}/messages/video", data=data, files=files, headers={"Content-Type": None})
88
+ print_json_obj(result)
89
+ return
90
+
91
+ payload = {"to": to, "video": url_or_path}
92
+ result = client.post(f"sessions/{session_id}/messages/video", json=payload)
93
+ print_json_obj(result)
94
+
95
+ @app.command("document")
96
+ def send_document_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
97
+ client = get_client(ctx)
98
+ if os.path.isfile(url_or_path):
99
+ with open(url_or_path, "rb") as f:
100
+ files = {"file": f}
101
+ data = {"to": to}
102
+ result = client.request("POST", f"sessions/{session_id}/messages/document", data=data, files=files, headers={"Content-Type": None})
103
+ print_json_obj(result)
104
+ return
105
+
106
+ payload = {"to": to, "document": url_or_path}
107
+ result = client.post(f"sessions/{session_id}/messages/document", json=payload)
108
+ print_json_obj(result)
109
+
110
+ @app.command("location")
111
+ def send_location_message(ctx: typer.Context, session_id: str, to: str, lat: float, lng: float):
112
+ client = get_client(ctx)
113
+ payload = {"to": to, "location": {"degreesLatitude": lat, "degreesLongitude": lng}}
114
+ result = client.post(f"sessions/{session_id}/messages/location", json=payload)
115
+ print_json_obj(result)
116
+
117
+ @app.command("contact")
118
+ def send_contact_message(ctx: typer.Context, session_id: str, to: str, vcard: str):
119
+ client = get_client(ctx)
120
+ payload = {"to": to, "vcard": vcard}
121
+ result = client.post(f"sessions/{session_id}/messages/contact", json=payload)
122
+ print_json_obj(result)
123
+
124
+ @app.command("poll")
125
+ def send_poll_message(ctx: typer.Context, session_id: str, to: str, question: str, options: str = typer.Option(..., help="Comma-separated options")):
126
+ client = get_client(ctx)
127
+ payload = {"to": to, "poll": {"name": question, "options": options.split(",")}}
128
+ result = client.post(f"sessions/{session_id}/messages/poll", json=payload)
129
+ print_json_obj(result)
130
+
131
+ @app.command("presence")
132
+ def send_presence_update(ctx: typer.Context, session_id: str, to: str, status: str = typer.Argument(..., help="available|composing|recording|paused")):
133
+ client = get_client(ctx)
134
+ payload = {"to": to, "presence": status}
135
+ result = client.post(f"sessions/{session_id}/messages/presence", json=payload)
136
+ print_json_obj(result)
137
+
138
+ @app.command("resend")
139
+ def resend_message(ctx: typer.Context, session_id: str, message_id: str):
140
+ client = get_client(ctx)
141
+ result = client.post(f"sessions/{session_id}/messages/{message_id}/resend")
142
+ print_json_obj(result)
143
+
144
+ @app.command("edit")
145
+ def edit_message(ctx: typer.Context, session_id: str, message_id: str, new_text: str):
146
+ client = get_client(ctx)
147
+ payload = {"text": new_text}
148
+ result = client.patch(f"sessions/{session_id}/messages/{message_id}", json=payload)
149
+ print_json_obj(result)
150
+
151
+ @app.command("delete")
152
+ def delete_message(ctx: typer.Context, session_id: str, message_id: str):
153
+ client = get_client(ctx)
154
+ result = client.delete(f"sessions/{session_id}/messages/{message_id}")
155
+ print_json_obj(result)
156
+
157
+ @app.command("info")
158
+ def get_message_info(ctx: typer.Context, session_id: str, message_id: str):
159
+ client = get_client(ctx)
160
+ result = client.get(f"sessions/{session_id}/messages/{message_id}")
161
+ print_json_obj(result)
wasender/output.py ADDED
@@ -0,0 +1,31 @@
1
+ import sys
2
+ from rich.console import Console
3
+
4
+ console = Console()
5
+
6
+ _raw = False
7
+ _quiet = False
8
+
9
+ def set_output_mode(raw: bool, quiet: bool):
10
+ global _raw, _quiet
11
+ _raw = raw
12
+ _quiet = quiet
13
+
14
+ def print_json_obj(data):
15
+ if _quiet:
16
+ return
17
+ if _raw:
18
+ import json
19
+ print(json.dumps(data))
20
+ else:
21
+ console.print_json(data=data)
22
+
23
+ def print_error_and_exit(message: str, code: int = 1):
24
+ if not _quiet:
25
+ console.print(f"[bold red]Error[/bold red]: {message}")
26
+ sys.exit(code)
27
+
28
+ def print_http_error_and_exit(status_code: int, message: str):
29
+ if not _quiet:
30
+ console.print(f"[bold red]Error {status_code}[/bold red]: {message}")
31
+ sys.exit(1)
wasender/sessions.py ADDED
@@ -0,0 +1,72 @@
1
+ import httpx
2
+ import typer
3
+ from typing import Optional
4
+ from .output import print_json_obj
5
+ from .client import WasenderClient
6
+
7
+ app = typer.Typer()
8
+
9
+ def get_client(ctx: typer.Context) -> WasenderClient:
10
+ return ctx.obj
11
+
12
+ @app.command("list")
13
+ def list_sessions(ctx: typer.Context, page: int = 1, limit: int = 10):
14
+ client = get_client(ctx)
15
+ result = client.get("sessions", params={"page": page, "limit": limit})
16
+ print_json_obj(result)
17
+
18
+ @app.command("get")
19
+ def get_session(ctx: typer.Context, session_id: str):
20
+ client = get_client(ctx)
21
+ result = client.get(f"sessions/{session_id}")
22
+ print_json_obj(result)
23
+
24
+ @app.command("user")
25
+ def get_session_user(ctx: typer.Context, session_id: str):
26
+ client = get_client(ctx)
27
+ result = client.get(f"sessions/{session_id}/user")
28
+ print_json_obj(result)
29
+
30
+ @app.command("create")
31
+ def create_session(ctx: typer.Context, name: str = typer.Option(..., prompt=True), webhook: Optional[str] = None):
32
+ client = get_client(ctx)
33
+ payload = {"name": name}
34
+ if webhook:
35
+ payload["webhook"] = webhook
36
+ result = client.post("sessions", json=payload)
37
+ print_json_obj(result)
38
+
39
+ @app.command("update")
40
+ def update_session(ctx: typer.Context, session_id: str, name: Optional[str] = None, webhook: Optional[str] = None):
41
+ client = get_client(ctx)
42
+ payload = {}
43
+ if name is not None:
44
+ payload["name"] = name
45
+ if webhook is not None:
46
+ payload["webhook"] = webhook
47
+ result = client.patch(f"sessions/{session_id}", json=payload)
48
+ print_json_obj(result)
49
+
50
+ @app.command("connect")
51
+ def connect_session(ctx: typer.Context, session_id: str):
52
+ client = get_client(ctx)
53
+ result = client.post(f"sessions/{session_id}/connect")
54
+ print_json_obj(result)
55
+
56
+ @app.command("disconnect")
57
+ def disconnect_session(ctx: typer.Context, session_id: str):
58
+ client = get_client(ctx)
59
+ result = client.post(f"sessions/{session_id}/disconnect")
60
+ print_json_obj(result)
61
+
62
+ @app.command("message-logs")
63
+ def get_message_logs(ctx: typer.Context, session_id: str):
64
+ client = get_client(ctx)
65
+ result = client.get(f"sessions/{session_id}/message-logs")
66
+ print_json_obj(result)
67
+
68
+ @app.command("session-logs")
69
+ def get_session_logs(ctx: typer.Context, session_id: str):
70
+ client = get_client(ctx)
71
+ result = client.get(f"sessions/{session_id}/logs")
72
+ print_json_obj(result)
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: wasender-cli
3
+ Version: 0.1.0
4
+ Summary: A production-grade CLI tool for the WasenderAPI
5
+ Author-email: Tapatio Solutions <girishdudhwal@gmail.com>
6
+ License: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Environment :: Console
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: typer>=0.9.0
15
+ Requires-Dist: httpx>=0.25.0
16
+ Requires-Dist: rich>=13.0.0
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Dynamic: license-file
19
+
20
+ # Wasender CLI
21
+
22
+ A production-grade CLI tool for the WasenderAPI using Python with Typer.
23
+
24
+ ## Installation
25
+
26
+ 1. Make sure you have Python 3.9+ installed.
27
+ 2. Install the CLI in editable mode:
28
+ ```bash
29
+ pip install -e .
30
+ ```
31
+
32
+ ## Setup
33
+
34
+ You must set your Wasender API token. You can pass it via `--token`, set an environment variable, or use a `.env` file.
35
+
36
+ **Option 1: `.env` file (Recommended)**
37
+ Create a `.env` file in the directory where you run the CLI:
38
+ ```ini
39
+ WASENDER_TOKEN=your_token_here
40
+ ```
41
+
42
+ **Option 2: Environment Variable**
43
+ ```bash
44
+ export WASENDER_TOKEN=your_token_here
45
+ # On Windows PowerShell:
46
+ # $env:WASENDER_TOKEN="your_token_here"
47
+ ```
48
+
49
+ ## Examples
50
+
51
+ ### Sessions Group
52
+ ```bash
53
+ wasender sessions list
54
+ wasender sessions create --name "My Session"
55
+ ```
56
+
57
+ ### Messages Group
58
+ ```bash
59
+ wasender messages text my_session 1234567890 "Hello from Wasender CLI!"
60
+ wasender messages image my_session 1234567890 https://example.com/image.png
61
+ ```
62
+
63
+ ### Contacts Group
64
+ ```bash
65
+ wasender contacts list my_session
66
+ wasender contacts add my_session 1234567890 "John Doe"
67
+ ```
68
+
69
+ ### Groups Group
70
+ ```bash
71
+ wasender groups list my_session
72
+ wasender groups create my_session "Test Group" --participants 1234567890@s.whatsapp.net,0987654321@s.whatsapp.net
73
+ ```
74
+
75
+ Use `wasender --help` or `wasender <group> --help` to see all available commands and options.
@@ -0,0 +1,14 @@
1
+ wasender/__init__.py,sha256=S9eLObttReEuA_m5THSNfyQrTbKLm40FyO8rfvYGLkk,49
2
+ wasender/client.py,sha256=y7Uh50ejidcXtvJdBLMR3rhxXh_IrInUIzV7sKxU9p8,2408
3
+ wasender/contacts.py,sha256=fT6ZuthZUqNA50JqAStdhR5mYpZRG0JLRWBSa9xeV6w,2450
4
+ wasender/groups.py,sha256=Cdpug5_FfcAtXd79UiucBn7xiwf37DBzUaeWzqyTuTk,4410
5
+ wasender/main.py,sha256=WxsYyYpECRWUaLt6oOILFZ38GE98CTkEKyIxjsPErY4,1767
6
+ wasender/messages.py,sha256=nnus0EcXNtjuQI7zEm7tlBhvz7bm71ctvCgTTLnIDDA,7011
7
+ wasender/output.py,sha256=BCPw4fOLkgKeUqzDHpJD4RcqGU4QjhvP8-4nr2Zy2ws,703
8
+ wasender/sessions.py,sha256=-e_bUS6zSEo3fWxOIWAyDaSQwcHdYg7u4U-KtRGpvQo,2357
9
+ wasender_cli-0.1.0.dist-info/licenses/LICENSE,sha256=77T62lnNdRZj0p9ri4jCBWdP3mKnY-z5R1WjtOgOgjk,1074
10
+ wasender_cli-0.1.0.dist-info/METADATA,sha256=8aEH6E8ezSaOiwAgUo7-ulFB_KQF3otlwqge0v3uh1g,2015
11
+ wasender_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ wasender_cli-0.1.0.dist-info/entry_points.txt,sha256=Q1VSEyDfTa04fHLne7uAAo-awfehw9Doa--iBTQEqYY,47
13
+ wasender_cli-0.1.0.dist-info/top_level.txt,sha256=pEMIDpGhre1TscVONhw6I9mVWgI2DQdJH9JXE3wKRv8,9
14
+ wasender_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wasender = wasender.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tapatio Solutions
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
+ wasender