gixenpy 0.2.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.
gixenpy/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """gixenpy — unofficial Python client for Gixen.com (eBay sniping)."""
2
+ from .client import (
3
+ GixenClient,
4
+ GixenError,
5
+ Snipe,
6
+ SnipeForm,
7
+ SnipeResult,
8
+ legacy_item_number,
9
+ )
10
+
11
+ __all__ = [
12
+ "GixenClient",
13
+ "GixenError",
14
+ "Snipe",
15
+ "SnipeForm",
16
+ "SnipeResult",
17
+ "legacy_item_number",
18
+ ]
19
+
20
+ __version__ = "0.2.0"
gixenpy/cli.py ADDED
@@ -0,0 +1,206 @@
1
+ """gixenpy CLI: manage Gixen snipes from the terminal.
2
+
3
+ Credentials via the GIXEN_USERNAME / GIXEN_PASSWORD environment variables.
4
+ If a `.env` file exists in the current directory, it's loaded before reading
5
+ them (see `_load_dotenv`); a variable already exported in the environment
6
+ takes priority over the `.env` file.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from . import __version__
17
+ from .client import GixenClient, GixenError, Snipe
18
+
19
+
20
+ def _load_dotenv(path: str = ".env") -> None:
21
+ """
22
+ Loads `KEY=value` variables from a simple `.env` file, one per line
23
+ (leading '#' = comment). Doesn't add python-dotenv as a dependency since
24
+ only two variables are needed; doesn't override ones already in the
25
+ environment, so `export GIXEN_USERNAME=...` still wins over the `.env`.
26
+ """
27
+ p = Path(path)
28
+ if not p.is_file():
29
+ return
30
+ for line in p.read_text().splitlines():
31
+ line = line.strip()
32
+ if not line or line.startswith("#") or "=" not in line:
33
+ continue
34
+ key, _, value = line.partition("=")
35
+ key = key.strip()
36
+ value = value.strip().strip('"').strip("'")
37
+ if key:
38
+ os.environ.setdefault(key, value)
39
+
40
+
41
+ def _client_from_env() -> GixenClient:
42
+ _load_dotenv()
43
+ return GixenClient(
44
+ username=os.environ.get("GIXEN_USERNAME", ""),
45
+ password=os.environ.get("GIXEN_PASSWORD", ""),
46
+ dry_run=False,
47
+ )
48
+
49
+
50
+ def _fail(message: str) -> None:
51
+ click.secho(f"ERROR: {message}", fg="red", err=True)
52
+ sys.exit(1)
53
+
54
+
55
+ def _ok(message: str) -> None:
56
+ click.secho(f"OK: {message}", fg="green")
57
+
58
+
59
+ @click.group()
60
+ @click.version_option(__version__, prog_name="gixenpy")
61
+ def main() -> None:
62
+ """Manage Gixen.com (eBay sniping) snipes from the terminal.
63
+
64
+ Reads credentials from GIXEN_USERNAME / GIXEN_PASSWORD (environment
65
+ variables or a .env file in the current directory).
66
+ """
67
+
68
+
69
+ def _snipe_row(s: Snipe) -> str:
70
+ offset = s.offset or "-"
71
+ group = s.group or "-"
72
+ return f"{s.item_id:<14} {s.max_bid:>10} offset={offset:<4} group={group:<3} {s.ebay_url}"
73
+
74
+
75
+ def _print_section(title: str, snipes: list[Snipe]) -> None:
76
+ click.secho(f"\n{title} ({len(snipes)})", bold=True)
77
+ if not snipes:
78
+ click.echo(" (none)")
79
+ return
80
+ for s in snipes:
81
+ click.echo(" " + _snipe_row(s))
82
+
83
+
84
+ @main.command("list")
85
+ def list_cmd() -> None:
86
+ """List the account's snipes, split into active and ended."""
87
+ client = _client_from_env()
88
+ try:
89
+ snipes = client.list_snipes()
90
+ except GixenError as e:
91
+ _fail(str(e))
92
+ return
93
+ active = [s for s in snipes if s.status == "active"]
94
+ ended = [s for s in snipes if s.status == "ended"]
95
+ unknown = [s for s in snipes if s.status == "unknown"]
96
+ _print_section("Active", active)
97
+ _print_section("Ended", ended)
98
+ if unknown:
99
+ _print_section("Unknown status", unknown)
100
+
101
+
102
+ @main.command("add")
103
+ @click.argument("item")
104
+ @click.argument("max_bid", type=float)
105
+ @click.option("--offset", type=int, default=None, help="Seconds before the close.")
106
+ @click.option("--group", type=int, default=None, help="Bid group (0 = no group).")
107
+ @click.option("--dry-run", is_flag=True, default=False,
108
+ help="Don't schedule anything: show what would be sent.")
109
+ def add_cmd(item: str, max_bid: float, offset: int | None, group: int | None,
110
+ dry_run: bool) -> None:
111
+ """Schedule a new snipe for ITEM with max bid MAX_BID."""
112
+ client = _client_from_env()
113
+ try:
114
+ res = client.add_snipe(item, max_bid, offset=offset, group=group, dry_run=dry_run)
115
+ except GixenError as e:
116
+ _fail(str(e))
117
+ return
118
+ if dry_run:
119
+ click.echo(res.message)
120
+ click.echo(f"payload: {res.payload}")
121
+ return
122
+ if res.ok:
123
+ _ok(res.message)
124
+ else:
125
+ _fail(res.message)
126
+
127
+
128
+ @main.command("edit")
129
+ @click.argument("item")
130
+ @click.argument("max_bid", type=float, required=False, default=None)
131
+ @click.option("--offset", type=int, default=None, help="Seconds before the close.")
132
+ @click.option("--group", type=int, default=None, help="Bid group (0 = no group).")
133
+ def edit_cmd(item: str, max_bid: float | None, offset: int | None, group: int | None) -> None:
134
+ """Change the bid/offset/group of an already-scheduled snipe for ITEM.
135
+
136
+ MAX_BID is optional: you can change only --offset and/or --group
137
+ without touching it.
138
+ """
139
+ client = _client_from_env()
140
+ try:
141
+ res = client.update_snipe(item, new_max=max_bid, offset=offset, group=group)
142
+ except GixenError as e:
143
+ _fail(str(e))
144
+ return
145
+ if res.ok:
146
+ _ok(res.message)
147
+ else:
148
+ _fail(res.message)
149
+
150
+
151
+ @main.command("remove")
152
+ @click.argument("item")
153
+ def remove_cmd(item: str) -> None:
154
+ """Delete the scheduled snipe for ITEM."""
155
+ client = _client_from_env()
156
+ try:
157
+ res = client.delete_snipe(item)
158
+ except GixenError as e:
159
+ _fail(str(e))
160
+ return
161
+ if res.ok:
162
+ _ok(res.message)
163
+ else:
164
+ _fail(res.message)
165
+
166
+
167
+ @main.command("purge")
168
+ def purge_cmd() -> None:
169
+ """Purge already-ended snipes from the list (doesn't affect active ones)."""
170
+ client = _client_from_env()
171
+ try:
172
+ res = client.purge_completed()
173
+ except GixenError as e:
174
+ _fail(str(e))
175
+ return
176
+ if res.ok:
177
+ _ok(res.message)
178
+ else:
179
+ _fail(res.message)
180
+
181
+
182
+ @main.command("group")
183
+ @click.argument("group_id", type=int)
184
+ @click.argument("items", nargs=-1, required=True)
185
+ def group_cmd(group_id: int, items: tuple[str, ...]) -> None:
186
+ """Assign group GROUP_ID to one or more ITEMS."""
187
+ client = _client_from_env()
188
+ failures = 0
189
+ for item in items:
190
+ try:
191
+ res = client.update_snipe(item, group=group_id)
192
+ except GixenError as e:
193
+ click.secho(f"ERROR: {item}: {e}", fg="red", err=True)
194
+ failures += 1
195
+ continue
196
+ if res.ok:
197
+ click.secho(f"OK: {item}: {res.message}", fg="green")
198
+ else:
199
+ click.secho(f"ERROR: {item}: {res.message}", fg="red", err=True)
200
+ failures += 1
201
+ if failures:
202
+ sys.exit(1)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()