xitzin 0.4.0__py3-none-any.whl → 0.5.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.
xitzin/__init__.py
CHANGED
|
@@ -22,6 +22,7 @@ Example:
|
|
|
22
22
|
|
|
23
23
|
from .application import Xitzin
|
|
24
24
|
from .cgi import CGIConfig, CGIHandler, CGIScript
|
|
25
|
+
from .middleware import VirtualHostMiddleware
|
|
25
26
|
from .scgi import SCGIApp, SCGIConfig, SCGIHandler
|
|
26
27
|
from .exceptions import (
|
|
27
28
|
BadRequest,
|
|
@@ -55,6 +56,8 @@ __all__ = [
|
|
|
55
56
|
"Input",
|
|
56
57
|
"Redirect",
|
|
57
58
|
"Link",
|
|
59
|
+
# Middleware
|
|
60
|
+
"VirtualHostMiddleware",
|
|
58
61
|
# CGI support
|
|
59
62
|
"CGIConfig",
|
|
60
63
|
"CGIHandler",
|
xitzin/application.py
CHANGED
|
@@ -399,6 +399,69 @@ class Xitzin:
|
|
|
399
399
|
|
|
400
400
|
self.mount(path, handler, name=name)
|
|
401
401
|
|
|
402
|
+
def vhost(
|
|
403
|
+
self,
|
|
404
|
+
hosts: dict[str, "Xitzin"],
|
|
405
|
+
*,
|
|
406
|
+
default_app: "Xitzin | None" = None,
|
|
407
|
+
fallback_status: int = 53,
|
|
408
|
+
fallback_handler: Callable[[Request], Any] | None = None,
|
|
409
|
+
) -> None:
|
|
410
|
+
"""Configure virtual hosting for this application.
|
|
411
|
+
|
|
412
|
+
This is a convenience method that creates and registers VirtualHostMiddleware.
|
|
413
|
+
The middleware routes requests to different apps based on hostname.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
hosts: Mapping of hostname patterns to Xitzin apps.
|
|
417
|
+
Patterns can be exact ("example.com") or wildcards ("*.example.com").
|
|
418
|
+
Exact matches are checked first, then wildcards in definition order.
|
|
419
|
+
default_app: Default app when no pattern matches.
|
|
420
|
+
fallback_status: Status code for unmatched hosts (default: 53).
|
|
421
|
+
Common values: 53 (Proxy Refused), 51 (Not Found), 59 (Bad Request).
|
|
422
|
+
fallback_handler: Custom handler for unmatched hosts.
|
|
423
|
+
Takes precedence over default_app and fallback_status.
|
|
424
|
+
|
|
425
|
+
Example:
|
|
426
|
+
main_app = Xitzin(title="Main")
|
|
427
|
+
blog_app = Xitzin(title="Blog")
|
|
428
|
+
api_app = Xitzin(title="API")
|
|
429
|
+
|
|
430
|
+
@blog_app.gemini("/")
|
|
431
|
+
def blog_home(request: Request):
|
|
432
|
+
return "# Blog Home"
|
|
433
|
+
|
|
434
|
+
@api_app.gemini("/")
|
|
435
|
+
def api_home(request: Request):
|
|
436
|
+
return "# API Home"
|
|
437
|
+
|
|
438
|
+
@main_app.gemini("/")
|
|
439
|
+
def main_home(request: Request):
|
|
440
|
+
return "# Main Home"
|
|
441
|
+
|
|
442
|
+
# Configure as gateway
|
|
443
|
+
main_app.vhost({
|
|
444
|
+
"blog.example.com": blog_app,
|
|
445
|
+
"*.api.example.com": api_app,
|
|
446
|
+
}, default_app=main_app)
|
|
447
|
+
|
|
448
|
+
main_app.run()
|
|
449
|
+
"""
|
|
450
|
+
from .middleware import VirtualHostMiddleware
|
|
451
|
+
|
|
452
|
+
vhost_mw = VirtualHostMiddleware(
|
|
453
|
+
hosts,
|
|
454
|
+
default_app=default_app,
|
|
455
|
+
fallback_status=fallback_status,
|
|
456
|
+
fallback_handler=fallback_handler,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
@self.middleware
|
|
460
|
+
async def virtual_host_dispatcher(
|
|
461
|
+
request: Request, call_next: Callable[..., Any]
|
|
462
|
+
) -> GeminiResponse:
|
|
463
|
+
return await vhost_mw(request, call_next)
|
|
464
|
+
|
|
402
465
|
def on_startup(self, handler: Callable[[], Any]) -> Callable[[], Any]:
|
|
403
466
|
"""Register a startup event handler.
|
|
404
467
|
|
|
@@ -559,64 +622,56 @@ class Xitzin:
|
|
|
559
622
|
"""
|
|
560
623
|
request = Request(raw_request, self)
|
|
561
624
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
async def call_mounted_handler(req: Request) -> GeminiResponse:
|
|
625
|
+
# Build middleware chain around the entire routing logic
|
|
626
|
+
async def route_and_handle(req: Request) -> GeminiResponse:
|
|
627
|
+
try:
|
|
628
|
+
# Check mounted routes first
|
|
629
|
+
mount_match = self._router.match_mount(req.path)
|
|
630
|
+
if mount_match is not None:
|
|
631
|
+
mounted_route, path_info = mount_match
|
|
570
632
|
result = await mounted_route.call_handler(req, path_info)
|
|
571
633
|
return convert_response(result, req)
|
|
572
634
|
|
|
573
|
-
#
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return await handler(request)
|
|
635
|
+
# Match regular route
|
|
636
|
+
match = self._router.match(req.path)
|
|
637
|
+
if match is None:
|
|
638
|
+
raise NotFound(f"No route matches: {req.path}")
|
|
579
639
|
|
|
580
|
-
|
|
581
|
-
match = self._router.match(request.path)
|
|
582
|
-
if match is None:
|
|
583
|
-
raise NotFound(f"No route matches: {request.path}")
|
|
640
|
+
route, params = match
|
|
584
641
|
|
|
585
|
-
|
|
642
|
+
# Handle input flow
|
|
643
|
+
if route.input_prompt and not req.query:
|
|
644
|
+
return Input(
|
|
645
|
+
route.input_prompt, route.sensitive_input
|
|
646
|
+
).to_gemini_response()
|
|
586
647
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
route.input_prompt, route.sensitive_input
|
|
591
|
-
).to_gemini_response()
|
|
648
|
+
# Add query to params for input routes
|
|
649
|
+
if route.input_prompt and req.query:
|
|
650
|
+
params["query"] = req.query
|
|
592
651
|
|
|
593
|
-
|
|
594
|
-
if route.input_prompt and request.query:
|
|
595
|
-
params["query"] = request.query
|
|
596
|
-
|
|
597
|
-
# Build middleware chain
|
|
598
|
-
async def call_handler(req: Request) -> GeminiResponse:
|
|
652
|
+
# Call the handler
|
|
599
653
|
result = await route.call_handler(req, params)
|
|
600
654
|
return convert_response(result, req)
|
|
601
655
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
656
|
+
except GeminiException as e:
|
|
657
|
+
return GeminiResponse(status=e.status_code, meta=e.message)
|
|
658
|
+
except Exception:
|
|
659
|
+
# Log the error and return a generic failure
|
|
660
|
+
import traceback
|
|
606
661
|
|
|
607
|
-
|
|
662
|
+
traceback.print_exc()
|
|
663
|
+
return GeminiResponse(
|
|
664
|
+
status=StatusCode.TEMPORARY_FAILURE,
|
|
665
|
+
meta="Internal server error",
|
|
666
|
+
)
|
|
608
667
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
668
|
+
# Apply middleware around the entire routing logic
|
|
669
|
+
# This allows middleware to intercept requests before routing
|
|
670
|
+
handler = route_and_handle
|
|
671
|
+
for mw in reversed(self._middleware):
|
|
672
|
+
handler = self._wrap_middleware(mw, handler)
|
|
614
673
|
|
|
615
|
-
|
|
616
|
-
return GeminiResponse(
|
|
617
|
-
status=StatusCode.TEMPORARY_FAILURE,
|
|
618
|
-
meta="Internal server error",
|
|
619
|
-
)
|
|
674
|
+
return await handler(request)
|
|
620
675
|
|
|
621
676
|
async def _handle_titan_request(
|
|
622
677
|
self, raw_request: NauyacaTitanRequest
|
|
@@ -627,41 +682,42 @@ class Xitzin:
|
|
|
627
682
|
"""
|
|
628
683
|
request = TitanRequest(raw_request, self)
|
|
629
684
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
685
|
+
# Build middleware chain around the entire routing logic
|
|
686
|
+
async def route_and_handle(req: TitanRequest) -> GeminiResponse:
|
|
687
|
+
try:
|
|
688
|
+
# Match Titan route
|
|
689
|
+
match = self._router.match_titan(req.path)
|
|
690
|
+
if match is None:
|
|
691
|
+
raise NotFound(f"No Titan route matches: {req.path}")
|
|
635
692
|
|
|
636
|
-
|
|
693
|
+
route, params = match
|
|
637
694
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
695
|
+
# Validate auth token if required
|
|
696
|
+
if route.auth_tokens is not None:
|
|
697
|
+
if not req.token or req.token not in route.auth_tokens:
|
|
698
|
+
raise CertificateRequired("Valid authentication token required")
|
|
642
699
|
|
|
643
|
-
|
|
644
|
-
async def call_handler(req: TitanRequest) -> GeminiResponse:
|
|
700
|
+
# Call the handler
|
|
645
701
|
result = await route.call_handler(req, params)
|
|
646
702
|
return convert_response(result, req)
|
|
647
703
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
704
|
+
except GeminiException as e:
|
|
705
|
+
return GeminiResponse(status=e.status_code, meta=e.message)
|
|
706
|
+
except Exception:
|
|
707
|
+
import traceback
|
|
652
708
|
|
|
653
|
-
|
|
709
|
+
traceback.print_exc()
|
|
710
|
+
return GeminiResponse(
|
|
711
|
+
status=StatusCode.TEMPORARY_FAILURE,
|
|
712
|
+
meta="Internal server error",
|
|
713
|
+
)
|
|
654
714
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
715
|
+
# Apply middleware around the entire routing logic
|
|
716
|
+
handler = route_and_handle
|
|
717
|
+
for mw in reversed(self._middleware):
|
|
718
|
+
handler = self._wrap_middleware(mw, handler)
|
|
659
719
|
|
|
660
|
-
|
|
661
|
-
return GeminiResponse(
|
|
662
|
-
status=StatusCode.TEMPORARY_FAILURE,
|
|
663
|
-
meta="Internal server error",
|
|
664
|
-
)
|
|
720
|
+
return await handler(request)
|
|
665
721
|
|
|
666
722
|
def _wrap_middleware(
|
|
667
723
|
self,
|
xitzin/middleware.py
CHANGED
|
@@ -7,6 +7,7 @@ and modify responses before they are sent to clients.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
import re
|
|
10
11
|
import time
|
|
11
12
|
from abc import ABC
|
|
12
13
|
from collections import OrderedDict
|
|
@@ -18,6 +19,7 @@ from nauyaca.protocol.response import GeminiResponse
|
|
|
18
19
|
from nauyaca.protocol.status import StatusCode
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
22
|
+
from .application import Xitzin
|
|
21
23
|
from .requests import Request
|
|
22
24
|
|
|
23
25
|
# Type alias for middleware call_next function
|
|
@@ -382,3 +384,185 @@ class UserSessionMiddleware(BaseMiddleware):
|
|
|
382
384
|
currsize=len(self._async_cache),
|
|
383
385
|
)
|
|
384
386
|
return self._sync_cached_loader.cache_info()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class VirtualHostMiddleware(BaseMiddleware):
|
|
390
|
+
"""Middleware for hostname-based virtual hosting.
|
|
391
|
+
|
|
392
|
+
Routes requests to different Xitzin applications based on the hostname
|
|
393
|
+
in the request URL. Supports exact hostname matches and wildcard patterns.
|
|
394
|
+
|
|
395
|
+
Example:
|
|
396
|
+
from xitzin import Xitzin
|
|
397
|
+
from xitzin.middleware import VirtualHostMiddleware
|
|
398
|
+
|
|
399
|
+
blog_app = Xitzin(title="Blog")
|
|
400
|
+
api_app = Xitzin(title="API")
|
|
401
|
+
main_app = Xitzin(title="Gateway")
|
|
402
|
+
|
|
403
|
+
@blog_app.gemini("/")
|
|
404
|
+
def blog_home(request):
|
|
405
|
+
return "# Blog Home"
|
|
406
|
+
|
|
407
|
+
@api_app.gemini("/")
|
|
408
|
+
def api_home(request):
|
|
409
|
+
return "# API Home"
|
|
410
|
+
|
|
411
|
+
@main_app.gemini("/")
|
|
412
|
+
def main_home(request):
|
|
413
|
+
return "# Main Home"
|
|
414
|
+
|
|
415
|
+
# Create virtual host middleware
|
|
416
|
+
vhost_mw = VirtualHostMiddleware({
|
|
417
|
+
"blog.example.com": blog_app,
|
|
418
|
+
"*.api.example.com": api_app,
|
|
419
|
+
}, default_app=main_app)
|
|
420
|
+
|
|
421
|
+
@main_app.middleware
|
|
422
|
+
async def vhost(request, call_next):
|
|
423
|
+
return await vhost_mw(request, call_next)
|
|
424
|
+
|
|
425
|
+
main_app.run()
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
def __init__(
|
|
429
|
+
self,
|
|
430
|
+
hosts: dict[str, "Xitzin"],
|
|
431
|
+
*,
|
|
432
|
+
default_app: "Xitzin | None" = None,
|
|
433
|
+
fallback_status: int = 53,
|
|
434
|
+
fallback_handler: (
|
|
435
|
+
Callable[["Request"], Any] | Callable[["Request"], Awaitable[Any]] | None
|
|
436
|
+
) = None,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Create virtual host middleware.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
hosts: Mapping of hostname patterns to Xitzin apps.
|
|
442
|
+
Keys can be exact hostnames ("example.com") or wildcard patterns
|
|
443
|
+
("*.example.com"). Exact matches are checked first, then wildcards
|
|
444
|
+
in definition order.
|
|
445
|
+
default_app: Default app to use when no pattern matches.
|
|
446
|
+
Takes precedence over fallback_status.
|
|
447
|
+
fallback_status: Status code to return when no match and no default_app.
|
|
448
|
+
Defaults to 53 (Proxy Request Refused). Common values:
|
|
449
|
+
- 53: Proxy Request Refused (default)
|
|
450
|
+
- 51: Not Found
|
|
451
|
+
- 59: Bad Request
|
|
452
|
+
fallback_handler: Custom handler function for unmatched hosts.
|
|
453
|
+
Receives the request and must return a response. Takes precedence
|
|
454
|
+
over both default_app and fallback_status. Can be sync or async.
|
|
455
|
+
"""
|
|
456
|
+
self._default_app = default_app
|
|
457
|
+
self._fallback_status = fallback_status
|
|
458
|
+
self._fallback_handler = fallback_handler
|
|
459
|
+
self._is_fallback_async = (
|
|
460
|
+
iscoroutinefunction(fallback_handler) if fallback_handler else False
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Separate exact and wildcard patterns for efficiency
|
|
464
|
+
self._exact_hosts: dict[str, "Xitzin"] = {}
|
|
465
|
+
self._wildcard_patterns: list[tuple[re.Pattern[str], "Xitzin"]] = []
|
|
466
|
+
|
|
467
|
+
for pattern, app in hosts.items():
|
|
468
|
+
if pattern.startswith("*."):
|
|
469
|
+
compiled = self._compile_wildcard_pattern(pattern)
|
|
470
|
+
if compiled:
|
|
471
|
+
self._wildcard_patterns.append((compiled, app))
|
|
472
|
+
else:
|
|
473
|
+
self._exact_hosts[pattern.lower()] = app
|
|
474
|
+
|
|
475
|
+
def _compile_wildcard_pattern(self, pattern: str) -> re.Pattern[str] | None:
|
|
476
|
+
"""Convert wildcard pattern to regex.
|
|
477
|
+
|
|
478
|
+
Supports patterns like:
|
|
479
|
+
- *.example.com -> matches "blog.example.com", "api.example.com"
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
pattern: Wildcard pattern string starting with "*.".
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Compiled regex pattern, or None if invalid.
|
|
486
|
+
"""
|
|
487
|
+
if not pattern.startswith("*."):
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
# Extract domain part after "*."
|
|
491
|
+
domain = pattern[2:]
|
|
492
|
+
# Escape special regex chars in domain
|
|
493
|
+
domain_escaped = re.escape(domain)
|
|
494
|
+
# Replace the wildcard with a pattern that matches any non-dot chars
|
|
495
|
+
regex = f"^[^.]+\\.{domain_escaped}$"
|
|
496
|
+
|
|
497
|
+
return re.compile(regex, re.IGNORECASE)
|
|
498
|
+
|
|
499
|
+
def _match_hostname(self, hostname: str) -> "Xitzin | None":
|
|
500
|
+
"""Find the app for a given hostname.
|
|
501
|
+
|
|
502
|
+
Checks exact matches first, then wildcard patterns in order.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
hostname: The hostname from the request.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Matching Xitzin app, or None if no match.
|
|
509
|
+
"""
|
|
510
|
+
hostname_lower = hostname.lower()
|
|
511
|
+
|
|
512
|
+
# Try exact match first
|
|
513
|
+
if hostname_lower in self._exact_hosts:
|
|
514
|
+
return self._exact_hosts[hostname_lower]
|
|
515
|
+
|
|
516
|
+
# Try wildcard patterns
|
|
517
|
+
for pattern, app in self._wildcard_patterns:
|
|
518
|
+
if pattern.match(hostname_lower):
|
|
519
|
+
return app
|
|
520
|
+
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
async def before_request(
|
|
524
|
+
self, request: "Request"
|
|
525
|
+
) -> "Request | GeminiResponse | None":
|
|
526
|
+
"""Route request to appropriate app based on hostname."""
|
|
527
|
+
from .requests import TitanRequest
|
|
528
|
+
from .responses import convert_response
|
|
529
|
+
|
|
530
|
+
hostname = request.hostname
|
|
531
|
+
|
|
532
|
+
# Find matching app
|
|
533
|
+
app = self._match_hostname(hostname)
|
|
534
|
+
|
|
535
|
+
# Handle no match
|
|
536
|
+
if app is None:
|
|
537
|
+
# Priority 1: Custom fallback handler
|
|
538
|
+
if self._fallback_handler:
|
|
539
|
+
if self._is_fallback_async:
|
|
540
|
+
result = await self._fallback_handler(request)
|
|
541
|
+
else:
|
|
542
|
+
loop = asyncio.get_running_loop()
|
|
543
|
+
result = await loop.run_in_executor(
|
|
544
|
+
None, self._fallback_handler, request
|
|
545
|
+
)
|
|
546
|
+
return convert_response(result, request)
|
|
547
|
+
|
|
548
|
+
# Priority 2: Default app
|
|
549
|
+
if self._default_app:
|
|
550
|
+
app = self._default_app
|
|
551
|
+
else:
|
|
552
|
+
# Priority 3: Fallback status
|
|
553
|
+
return GeminiResponse(
|
|
554
|
+
status=StatusCode(self._fallback_status),
|
|
555
|
+
meta="Host not configured for this server",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# If the matched app is the same as the request's app, return None
|
|
559
|
+
# to continue processing through the normal route chain (avoid recursion)
|
|
560
|
+
if request._app is not None and app is request._app:
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
# Dispatch to matched app
|
|
564
|
+
if isinstance(request, TitanRequest):
|
|
565
|
+
response = await app._handle_titan_request(request._raw_request)
|
|
566
|
+
else:
|
|
567
|
+
response = await app._handle_request(request._raw_request)
|
|
568
|
+
return response
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
xitzin/__init__.py,sha256=
|
|
2
|
-
xitzin/application.py,sha256=
|
|
1
|
+
xitzin/__init__.py,sha256=EqdQIoz53LTfw5UMeFF-PZmBLa9tHUFrgU4Y2brt2yA,1943
|
|
2
|
+
xitzin/application.py,sha256=ZTAsNQWaZsXf4CIXh7LP3t5KZUsHCgW5HmqYSs-mydU,30004
|
|
3
3
|
xitzin/auth.py,sha256=hHhrP_fYToY6J6CCU8vgAsWbYn6K16LUxkeDMzZTF2Q,5304
|
|
4
4
|
xitzin/cgi.py,sha256=nggSuQ0gXIKdBKWHqH_CNZ6UlVGaB3VAOu3ZRPc43ek,18046
|
|
5
5
|
xitzin/exceptions.py,sha256=82z-CjyC0FwFbo9hGTjjmurlL_Vd4rTVdgkmQoFLXT0,3883
|
|
6
|
-
xitzin/middleware.py,sha256=
|
|
6
|
+
xitzin/middleware.py,sha256=rES7C1gujvP-1evpXfqng9uZ91rzJdqfob-nvGBtTAo,19042
|
|
7
7
|
xitzin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
xitzin/requests.py,sha256=1mbm5F8oywLPNpSdh2inn7K1IEeI3H5meOwj8jldi1Q,7783
|
|
9
9
|
xitzin/responses.py,sha256=QVH4B-qVHP7hxi0hIrOJhdCiHDf4PDCpSZ1fAD8iNkk,6525
|
|
@@ -13,6 +13,6 @@ xitzin/sqlmodel.py,sha256=lZvDzYAgnG8S2K-EYnx6PkO7D68pjM06uQLSKUZ9yY4,4500
|
|
|
13
13
|
xitzin/tasks.py,sha256=_smEXy-THge8wmQqWDtX3iUmAmoHniw_qqZBlKjCdqA,4597
|
|
14
14
|
xitzin/templating.py,sha256=spjxb05wgwKA4txOMbWJuwEVMaoLovo13_ukbXAcBDY,6040
|
|
15
15
|
xitzin/testing.py,sha256=wMiToopD2TAqTsreVqOrIUNCayORNQQennxR_GwMBSY,10980
|
|
16
|
-
xitzin-0.
|
|
17
|
-
xitzin-0.
|
|
18
|
-
xitzin-0.
|
|
16
|
+
xitzin-0.5.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
17
|
+
xitzin-0.5.0.dist-info/METADATA,sha256=vOmiKWl7qO1EAvFzFuQgRAK1QHJbMCulY_KoEKH_-3c,3456
|
|
18
|
+
xitzin-0.5.0.dist-info/RECORD,,
|