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 +20 -0
- gixenpy/cli.py +206 -0
- gixenpy/client.py +786 -0
- gixenpy/forms.py +88 -0
- gixenpy/py.typed +0 -0
- gixenpy-0.2.0.dist-info/METADATA +327 -0
- gixenpy-0.2.0.dist-info/RECORD +11 -0
- gixenpy-0.2.0.dist-info/WHEEL +5 -0
- gixenpy-0.2.0.dist-info/entry_points.txt +2 -0
- gixenpy-0.2.0.dist-info/licenses/LICENSE +21 -0
- gixenpy-0.2.0.dist-info/top_level.txt +1 -0
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()
|