kroxy 1.0.1__tar.gz → 1.0.2__tar.gz

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.
kroxy-1.0.2/PKG-INFO ADDED
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.4
2
+ Name: kroxy
3
+ Version: 1.0.2
4
+ Summary: A powerful Python toolkit for building automation, integrations, and bots.
5
+ Author-email: kroxy <kroxy@example.com>
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://pypi.org/project/kroxy
8
+ Keywords: bot,automation,antinuke,giveaway,music,scraper,toolkit,utility
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Internet
17
+ Classifier: Development Status :: 5 - Production/Stable
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: aiohttp>=3.8.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: build; extra == "dev"
24
+ Requires-Dist: twine; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # kroxy
28
+
29
+ > A powerful Python toolkit for building automation, integrations, and bots.
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/kroxy)](https://pypi.org/project/kroxy/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/kroxy)](https://pypi.org/project/kroxy/)
33
+ [![License](https://img.shields.io/badge/license-Proprietary-red)](https://pypi.org/project/kroxy/)
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install kroxy
41
+ pip install --upgrade kroxy
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Import Style
47
+
48
+ Everything in `kroxy` is importable directly from the top level.
49
+
50
+ ```python
51
+ # Everything from one place
52
+ import kroxy
53
+
54
+ # Discord
55
+ antinuke = kroxy.AntiNuke(whitelist=[OWNER_ID])
56
+ manager = kroxy.GiveawayManager()
57
+ api = kroxy.DiscordAPI(token="Bot TOKEN")
58
+ embed = kroxy.Utils.build_embed(title="Hello", color=0x5865F2)
59
+
60
+ # Website
61
+ async with kroxy.Fetcher() as f:
62
+ html = await f.get("https://example.com")
63
+
64
+ title = kroxy.Scraper.get_title(html)
65
+ slug = kroxy.WebUtils.slugify("Hello World")
66
+ ```
67
+
68
+ ```python
69
+ # Or import only what you need
70
+ from kroxy import AntiNuke, Checkers, Utils
71
+ from kroxy import GiveawayManager, MusicPlayerManager
72
+ from kroxy import DiscordAPI, SlashCommand, PrefixCommand
73
+ from kroxy import Fetcher, Scraper, WebUtils
74
+ ```
75
+
76
+ ```python
77
+ # Submodule access also works
78
+ from kroxy import discord, website
79
+
80
+ discord.AntiNuke(...)
81
+ website.Fetcher()
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Package Structure
87
+
88
+ ```
89
+ kroxy/
90
+ ├── discord/
91
+ │ ├── api.py ← Async Discord REST API client (v10)
92
+ │ ├── commands.py ← Slash and prefix command builders + registry
93
+ │ ├── utils.py ← Embeds, mentions, timestamps, permissions, text formatting
94
+ │ ├── antinuke.py ← Real-time nuke detection with auto-punishment
95
+ │ ├── checkers.py ← Permission, role, and hierarchy validation
96
+ │ ├── giveaway.py ← Weighted giveaway engine with scheduling and reroll
97
+ │ └── music.py ← Multi-guild music queue and player state manager
98
+ └── website/
99
+ ├── fetch.py ← Async HTTP GET/POST/download client
100
+ ├── scraper.py ← HTML parsing: links, images, meta, tables, text
101
+ └── utils.py ← URL validation, parsing, slugify, file type detection
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Module Reference
107
+
108
+ ---
109
+
110
+ ### `DiscordAPI` — Discord REST API Client
111
+
112
+ Async HTTP wrapper for the Discord REST API v10.
113
+
114
+ **Constructor:** `DiscordAPI(token: str, bot: bool = True)`
115
+
116
+ ```python
117
+ from kroxy import DiscordAPI
118
+
119
+ api = DiscordAPI(token="Bot YOUR_TOKEN")
120
+
121
+ # Guild & Channel
122
+ guild = await api.get_guild(GUILD_ID)
123
+ channel = await api.get_channel(CHANNEL_ID)
124
+ user = await api.get_user(USER_ID)
125
+
126
+ # Messaging
127
+ await api.send_message(CHANNEL_ID, content="Hello!")
128
+ await api.send_message(CHANNEL_ID, embed=embed_dict)
129
+ await api.send_message(CHANNEL_ID, content="Hi", components=[...])
130
+ await api.delete_message(CHANNEL_ID, MESSAGE_ID)
131
+
132
+ # Moderation
133
+ await api.ban_member(GUILD_ID, USER_ID, reason="Spam", delete_message_days=1)
134
+ await api.unban_member(GUILD_ID, USER_ID)
135
+ await api.kick_member(GUILD_ID, USER_ID, reason="AFK")
136
+
137
+ # Roles
138
+ await api.add_role(GUILD_ID, USER_ID, ROLE_ID)
139
+ await api.remove_role(GUILD_ID, USER_ID, ROLE_ID)
140
+
141
+ # Channels
142
+ await api.create_channel(GUILD_ID, name="general", channel_type=0)
143
+ await api.delete_channel(CHANNEL_ID)
144
+
145
+ # Audit & Invites & Webhooks
146
+ await api.get_audit_logs(GUILD_ID, limit=50)
147
+ await api.get_invites(GUILD_ID)
148
+ await api.delete_invite("invite_code")
149
+ await api.get_webhooks(GUILD_ID)
150
+ await api.delete_webhook(WEBHOOK_ID)
151
+
152
+ await api.close()
153
+ ```
154
+
155
+ ---
156
+
157
+ ### `AntiNuke` — Anti-Nuke System
158
+
159
+ Detects mass destructive actions per user and triggers automatic punishment.
160
+
161
+ **Constructor:** `AntiNuke(whitelist: list = [], limits: dict = {})`
162
+
163
+ **Default Limits:**
164
+
165
+ | Action | Threshold | Window |
166
+ |---|---|---|
167
+ | `ban` | 3 | 10s |
168
+ | `kick` | 3 | 10s |
169
+ | `channel_delete` | 3 | 10s |
170
+ | `channel_create` | 5 | 10s |
171
+ | `role_delete` | 3 | 10s |
172
+ | `role_create` | 5 | 10s |
173
+ | `webhook_create` | 3 | 10s |
174
+ | `webhook_delete` | 3 | 10s |
175
+ | `bot_add` | 2 | 30s |
176
+ | `mass_mention` | 5 | 5s |
177
+
178
+ ```python
179
+ from kroxy import AntiNuke
180
+
181
+ antinuke = AntiNuke(
182
+ whitelist=[OWNER_ID],
183
+ limits={"ban": (2, 5)} # override: 2 bans in 5s triggers
184
+ )
185
+
186
+ async def on_nuke(action, user_id, guild):
187
+ print(f"[NUKE] {action} by {user_id}")
188
+
189
+ antinuke.on_trigger = on_nuke
190
+ antinuke.punishment = "ban" # "ban" | "kick" | "strip_roles"
191
+
192
+ # Wire to your event system:
193
+ await antinuke.on_member_ban(user_id=uid, guild=guild)
194
+ await antinuke.on_member_kick(user_id=uid, guild=guild)
195
+ await antinuke.on_channel_delete(user_id=uid, guild=guild)
196
+ await antinuke.on_channel_create(user_id=uid, guild=guild)
197
+ await antinuke.on_role_delete(user_id=uid, guild=guild)
198
+ await antinuke.on_role_create(user_id=uid, guild=guild)
199
+ await antinuke.on_webhook_create(user_id=uid, guild=guild)
200
+ await antinuke.on_webhook_delete(user_id=uid, guild=guild)
201
+ await antinuke.on_bot_add(user_id=uid, guild=guild)
202
+ await antinuke.on_mass_mention(user_id=uid, guild=guild)
203
+
204
+ # Whitelist management
205
+ antinuke.add_whitelist(MOD_ID)
206
+ antinuke.remove_whitelist(MOD_ID)
207
+ antinuke.reset_user(USER_ID)
208
+ antinuke.get_stats() # current thresholds dict
209
+ ```
210
+
211
+ ---
212
+
213
+ ### `Checkers` — Permission & Role Validation
214
+
215
+ All methods raise `CheckFailed` on failure, return `True` on success.
216
+
217
+ ```python
218
+ from kroxy import Checkers, CheckFailed
219
+
220
+ try:
221
+ Checkers.require_admin(member.permissions)
222
+ Checkers.require_manage_guild(member.permissions)
223
+ Checkers.require_manage_roles(member.permissions)
224
+ Checkers.require_ban_members(member.permissions)
225
+ Checkers.require_kick_members(member.permissions)
226
+ Checkers.require_manage_channels(member.permissions)
227
+ Checkers.require_manage_messages(member.permissions)
228
+ Checkers.require_manage_webhooks(member.permissions)
229
+
230
+ Checkers.require_role(member.role_ids, MOD_ROLE_ID, "Moderator")
231
+ Checkers.require_any_role(member.role_ids, [MOD_ROLE_ID, ADMIN_ROLE_ID])
232
+ Checkers.has_all_roles(member.role_ids, [ROLE_A, ROLE_B])
233
+
234
+ Checkers.check_hierarchy(
235
+ executor_top_role_pos=member.top_role.position,
236
+ target_top_role_pos=target.top_role.position,
237
+ bot_top_role_pos=bot.top_role.position,
238
+ )
239
+
240
+ Checkers.require_guild_owner(user_id, guild.owner_id)
241
+ Checkers.require_guild_only(ctx.guild_id)
242
+ Checkers.require_dm_only(ctx.guild_id)
243
+ Checkers.block_bots(ctx.author.bot)
244
+ Checkers.require_nsfw(channel.nsfw)
245
+
246
+ except CheckFailed as e:
247
+ await ctx.send(f"❌ {e.message}")
248
+ ```
249
+
250
+ ---
251
+
252
+ ### `Utils` — Discord Utilities
253
+
254
+ Embed builder, mentions, timestamps, permissions, and text formatters.
255
+
256
+ ```python
257
+ from kroxy import Utils
258
+
259
+ # Embed builder
260
+ embed = Utils.build_embed(
261
+ title="Report",
262
+ description="Weekly summary",
263
+ color=0x5865F2,
264
+ fields=[
265
+ {"name": "Members", "value": "1,200", "inline": True},
266
+ {"name": "Messages", "value": "45,000", "inline": True},
267
+ ],
268
+ footer="kroxy",
269
+ thumbnail="https://example.com/icon.png",
270
+ image="https://example.com/banner.png",
271
+ author_name="Bot",
272
+ author_icon="https://example.com/bot.png",
273
+ timestamp=True,
274
+ )
275
+
276
+ # Mentions
277
+ Utils.mention_user(123456) # <@123456>
278
+ Utils.mention_role(654321) # <@&654321>
279
+ Utils.mention_channel(999999) # <#999999>
280
+ Utils.parse_mention("<@123456>") # 123456
281
+
282
+ # Timestamps
283
+ Utils.discord_timestamp(dt, style="R") # <t:...:R> relative
284
+ Utils.discord_timestamp(dt, style="F") # <t:...:F> full
285
+ Utils.time_until(3725) # "1 hour, 2 minutes, 5 seconds"
286
+ Utils.snowflake_to_timestamp(id) # datetime object
287
+
288
+ # Permissions
289
+ Utils.has_permission(bitfield, "administrator") # True/False
290
+ Utils.has_permission(bitfield, "ban_members")
291
+ Utils.permissions_list(bitfield) # list of names
292
+
293
+ # Text formatting
294
+ Utils.truncate(text, max_length=2048)
295
+ Utils.code_block("print('hi')", language="python")
296
+ Utils.inline_code("var")
297
+ Utils.bold("text") # **text**
298
+ Utils.italic("text") # *text*
299
+ Utils.underline("text") # __text__
300
+ Utils.strikethrough("text") # ~~text~~
301
+ Utils.spoiler("text") # ||text||
302
+ ```
303
+
304
+ ---
305
+
306
+ ### `GiveawayManager` — Giveaway Engine
307
+
308
+ Weighted giveaway system with bonus roles, auto-scheduling, and reroll.
309
+
310
+ ```python
311
+ from kroxy import GiveawayManager
312
+
313
+ manager = GiveawayManager()
314
+
315
+ async def on_end(giveaway, winners):
316
+ print(f"Winners of '{giveaway.prize}': {winners}")
317
+
318
+ manager.on_end = on_end
319
+
320
+ giveaway = await manager.create(
321
+ prize="Discord Nitro",
322
+ host_id=HOST_ID,
323
+ channel_id=CHANNEL_ID,
324
+ guild_id=GUILD_ID,
325
+ duration=86400, # seconds
326
+ winner_count=3,
327
+ required_role_id=MEMBER_ROLE_ID,
328
+ bonus_roles={
329
+ BOOSTER_ROLE_ID: 2, # +2 extra entries
330
+ VIP_ROLE_ID: 4, # +4 extra entries
331
+ },
332
+ )
333
+
334
+ giveaway.add_entry(user_id=555, role_ids=[BOOSTER_ROLE_ID])
335
+ giveaway.remove_entry(user_id=555)
336
+
337
+ giveaway.is_active # True/False
338
+ giveaway.time_remaining # seconds
339
+ giveaway.total_entries # weighted count
340
+ giveaway.participant_count # unique users
341
+ giveaway.giveaway_id # "A1B2C3D4"
342
+ giveaway.winners # list of user IDs after end
343
+
344
+ await manager.end_now(giveaway.giveaway_id)
345
+ await manager.cancel(giveaway.giveaway_id)
346
+ new_winners = giveaway.reroll()
347
+
348
+ manager.get(giveaway_id)
349
+ manager.get_by_message(message_id)
350
+ manager.all_active()
351
+ manager.all_ended()
352
+ ```
353
+
354
+ ---
355
+
356
+ ### `MusicPlayerManager` — Music Player
357
+
358
+ Multi-guild music queue and player state manager.
359
+
360
+ ```python
361
+ from kroxy import MusicPlayerManager, Track, LoopMode
362
+
363
+ manager = MusicPlayerManager()
364
+ player = manager.get_or_create(
365
+ guild_id=GUILD_ID,
366
+ channel_id=VOICE_CHANNEL_ID,
367
+ text_channel_id=TEXT_CHANNEL_ID,
368
+ )
369
+
370
+ track = Track(
371
+ title="Song Name",
372
+ url="https://youtube.com/watch?v=...",
373
+ stream_url="https://audio.stream/file.mp3",
374
+ duration=240,
375
+ requester_id=USER_ID,
376
+ thumbnail="https://img.youtube.com/vi/.../0.jpg",
377
+ source="youtube",
378
+ )
379
+
380
+ # Queue
381
+ player.queue.add(track)
382
+ player.queue.add_next(track)
383
+ player.queue.shuffle()
384
+ player.queue.remove(index=0)
385
+ player.queue.move(from_index=2, to_index=0)
386
+ player.queue.clear()
387
+ len(player.queue)
388
+ player.queue.total_duration
389
+ player.queue.is_empty
390
+
391
+ # Playback
392
+ await player.play_next()
393
+ await player.skip()
394
+ player.toggle_pause() # returns new paused state
395
+ player.set_volume(0.75) # 0.0 – 2.0
396
+ player.set_loop("track") # "none" | "track" | "queue"
397
+ player.stop()
398
+
399
+ # State
400
+ player.current # Track or None
401
+ player.paused # bool
402
+ player.volume # float
403
+ player.position # seconds elapsed
404
+ player.loop_mode # LoopMode enum
405
+ player.get_state() # full state dict
406
+
407
+ # Events
408
+ player.on_track_start = async_fn # called with (track, player)
409
+ player.on_track_end = async_fn # called with (track, player)
410
+ player.on_queue_empty = async_fn # called with (guild_id)
411
+
412
+ manager.remove(guild_id=GUILD_ID)
413
+ ```
414
+
415
+ ---
416
+
417
+ ### `SlashCommand` / `PrefixCommand` — Command Builders
418
+
419
+ ```python
420
+ from kroxy import SlashCommand, PrefixCommand, Option, CommandRegistry
421
+
422
+ @SlashCommand.decorator(
423
+ name="warn",
424
+ description="Warn a member",
425
+ options=[
426
+ Option("user", "Target member", option_type="user", required=True),
427
+ Option("reason", "Reason", required=False),
428
+ ],
429
+ permissions="8",
430
+ )
431
+ async def warn(interaction, user, reason="No reason"):
432
+ ...
433
+
434
+ @PrefixCommand.decorator(
435
+ name="ban",
436
+ aliases=["b", "yeet"],
437
+ cooldown=5,
438
+ permissions=["ban_members"],
439
+ )
440
+ async def ban(ctx, member, *, reason="No reason"):
441
+ ...
442
+
443
+ warn.to_dict() # export for Discord API registration
444
+
445
+ registry = CommandRegistry()
446
+ registry.add_slash(warn)
447
+ registry.add_prefix(ban)
448
+ registry.get_slash("warn")
449
+ registry.get_prefix("b") # alias lookup works
450
+ registry.all_slash()
451
+ registry.all_prefix()
452
+ ```
453
+
454
+ ---
455
+
456
+ ### `Fetcher` — Async HTTP Client
457
+
458
+ ```python
459
+ from kroxy import Fetcher
460
+
461
+ # As context manager (auto-closes session)
462
+ async with Fetcher() as f:
463
+ html = await f.get("https://example.com")
464
+ data = await f.get_json("https://api.example.com/data")
465
+ response = await f.post("https://example.com/form", data={"key": "value"})
466
+ result = await f.post_json("https://api.example.com", json={"q": "hello"})
467
+ headers = await f.head("https://example.com")
468
+ code = await f.status("https://example.com") # 200
469
+ ok = await f.is_reachable("https://example.com") # True/False
470
+ path = await f.download("https://example.com/file.zip", save_path="./file.zip")
471
+ raw = await f.get_bytes("https://example.com/image.png")
472
+ final = await f.resolve_redirect("https://bit.ly/abc")
473
+
474
+ # With custom headers and timeout
475
+ async with Fetcher(headers={"Authorization": "Bearer TOKEN"}, timeout=30) as f:
476
+ data = await f.get_json("https://api.example.com/protected")
477
+ ```
478
+
479
+ ---
480
+
481
+ ### `Scraper` — HTML Parser
482
+
483
+ All methods are static — pass raw HTML strings, get structured data back.
484
+
485
+ ```python
486
+ from kroxy import Scraper, Fetcher
487
+
488
+ async with Fetcher() as f:
489
+ html = await f.get("https://example.com")
490
+
491
+ Scraper.get_title(html) # "Example Domain"
492
+ Scraper.get_description(html) # meta description
493
+ Scraper.get_meta(html) # {"og:title": ..., "description": ...}
494
+ Scraper.get_og(html) # {"og:title": ..., "og:image": ...}
495
+
496
+ Scraper.get_links(html, base_url="https://example.com")
497
+ # [{"href": "https://example.com/page", "text": "Click here"}, ...]
498
+
499
+ Scraper.get_links(html, base_url="https://example.com", internal_only=True)
500
+ Scraper.get_links(html, base_url="https://example.com", external_only=True)
501
+
502
+ Scraper.get_images(html, base_url="https://example.com")
503
+ # [{"src": "...", "alt": "...", "width": "...", "height": "..."}, ...]
504
+
505
+ Scraper.get_tables(html)
506
+ # [[[row1col1, row1col2], [row2col1, row2col2]], ...]
507
+
508
+ Scraper.get_text(html) # plain text, no tags
509
+ Scraper.find_by_tag(html, "h1") # ["Heading 1", "Heading 2"]
510
+ Scraper.find_by_id(html, "main") # inner HTML of id="main"
511
+ Scraper.find_by_class(html, "card") # list of inner HTML strings
512
+ Scraper.extract_emails(html) # ["hello@example.com", ...]
513
+ Scraper.extract_phone_numbers(html) # ["+1 800 555 0000", ...]
514
+ Scraper.word_count(html) # 342
515
+ ```
516
+
517
+ ---
518
+
519
+ ### `WebUtils` — URL Utilities
520
+
521
+ All methods are static.
522
+
523
+ ```python
524
+ from kroxy import WebUtils
525
+
526
+ # Validation
527
+ WebUtils.is_valid_url("https://example.com") # True
528
+ WebUtils.is_https("https://example.com") # True
529
+ WebUtils.is_http("http://example.com") # True
530
+
531
+ # Parsing
532
+ WebUtils.get_domain("https://www.example.com/path") # "www.example.com"
533
+ WebUtils.get_base_domain("https://www.example.com/path") # "example.com"
534
+ WebUtils.get_scheme("https://example.com") # "https"
535
+ WebUtils.get_path("https://example.com/foo/bar") # "/foo/bar"
536
+ WebUtils.get_query_params("https://example.com?q=hi&page=2")
537
+ # {"q": ["hi"], "page": ["2"]}
538
+ WebUtils.get_tld("https://www.example.co.uk") # "uk"
539
+
540
+ # Transformation
541
+ WebUtils.add_query_params("https://example.com", {"q": "hello", "page": "2"})
542
+ WebUtils.remove_query_params("https://example.com?q=hi") # "https://example.com"
543
+ WebUtils.join("https://example.com/foo", "../bar") # "https://example.com/bar"
544
+ WebUtils.to_https("http://example.com") # "https://example.com"
545
+ WebUtils.normalize("www.example.com/path/") # "https://example.com/path"
546
+ WebUtils.encode("hello world!") # "hello%20world%21"
547
+
548
+ # Slug
549
+ WebUtils.slugify("Hello World! 123") # "hello-world-123"
550
+
551
+ # File type detection
552
+ WebUtils.get_file_extension("https://example.com/image.png?v=1") # "png"
553
+ WebUtils.is_image_url("https://example.com/photo.jpg") # True
554
+ WebUtils.is_video_url("https://example.com/video.mp4") # True
555
+ WebUtils.is_audio_url("https://example.com/track.mp3") # True
556
+
557
+ # Misc
558
+ WebUtils.extract_urls("Visit https://example.com or https://test.com now")
559
+ # ["https://example.com", "https://test.com"]
560
+ WebUtils.is_same_domain("https://example.com/a", "https://example.com/b") # True
561
+ ```
562
+
563
+ ---
564
+
565
+ ## Combined Example
566
+
567
+ ```python
568
+ import kroxy
569
+ import asyncio
570
+
571
+ async def main():
572
+ # Fetch a page
573
+ async with kroxy.Fetcher() as f:
574
+ html = await f.get("https://example.com")
575
+ api_data = await f.get_json("https://api.example.com/stats")
576
+
577
+ # Scrape it
578
+ title = kroxy.Scraper.get_title(html)
579
+ links = kroxy.Scraper.get_links(html, base_url="https://example.com")
580
+ emails = kroxy.Scraper.extract_emails(html)
581
+
582
+ # Validate a URL
583
+ if kroxy.WebUtils.is_valid_url("https://example.com"):
584
+ slug = kroxy.WebUtils.slugify(title)
585
+
586
+ # Send results to Discord
587
+ api = kroxy.DiscordAPI(token="Bot TOKEN")
588
+ embed = kroxy.Utils.build_embed(
589
+ title=title,
590
+ description=f"Found {len(links)} links, {len(emails)} emails",
591
+ color=0x5865F2,
592
+ timestamp=True,
593
+ )
594
+ await api.send_message(CHANNEL_ID, embed=embed)
595
+ await api.close()
596
+
597
+ asyncio.run(main())
598
+ ```
599
+
600
+ ---
601
+
602
+ ## Requirements
603
+
604
+ - Python **3.9+**
605
+ - `aiohttp >= 3.8.0`
606
+
607
+ ---
608
+
609
+ ## Versioning
610
+
611
+ See [VERSIONS.md](https://pypi.org/project/kroxy/) for the full changelog.
612
+
613
+ | Version | What changed |
614
+ |---|---|
615
+ | 1.0.2 | `kroxy.website` — Fetcher, Scraper, WebUtils |
616
+ | 1.0.1 | All classes importable from `kroxy` top level |
617
+ | 1.0.0 | Proprietary license, professional metadata |
618
+ | 0.1.0 | Initial release — discord module |
619
+
620
+ ---
621
+
622
+ ## License
623
+
624
+ **Proprietary** — Copyright © 2026 kroxy. All rights reserved.
625
+
626
+ Unauthorized copying, redistribution, or modification is strictly prohibited.
627
+ Contact **@kroxy** for permissions.