chanx 2.2.2__tar.gz → 2.3.0__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.
- {chanx-2.2.2 → chanx-2.3.0}/PKG-INFO +1 -1
- chanx-2.3.0/chanx/channels/management/__init__.py +1 -0
- chanx-2.3.0/chanx/channels/management/commands/__init__.py +1 -0
- chanx-2.3.0/chanx/channels/management/commands/generate_asyncapi_schema.py +259 -0
- {chanx-2.2.2 → chanx-2.3.0}/pyproject.toml +1 -1
- {chanx-2.2.2 → chanx-2.3.0}/.gitignore +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/AUTHORS.rst +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/LICENSE +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/README.rst +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/asyncapi/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/asyncapi/constants.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/asyncapi/generator.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/asyncapi/type_defs.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/apps.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/authenticator.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/discovery.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/routing.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/settings.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/templates/channels/asyncapi_docs.html +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/testing.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/urls.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/utils/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/utils/asgi.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/utils/request.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/utils/settings.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/views.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/channels/websocket.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/cli/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/cli/main.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/analyzer.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/base/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/base/client.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/codegen.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/generator.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/loader.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/client_generator/templates.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/constants.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/authenticator.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/check.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/config.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/decorators.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/registry.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/testing.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/core/websocket.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/constants.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/discovery.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/testing.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/type_defs.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/utils.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/views.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/fast_channels/websocket.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/messages/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/messages/base.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/messages/incoming.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/messages/outgoing.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/py.typed +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/routing/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/routing/discovery.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/routing/patterns.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/routing/traversal.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/type_defs.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/utils/__init__.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/utils/asyncio.py +0 -0
- {chanx-2.2.2 → chanx-2.3.0}/chanx/utils/logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chanx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: The missing toolkit for Channels — auth, logging, consumers, and more.
|
|
5
5
|
Project-URL: Documentation, https://chanx.readthedocs.io/
|
|
6
6
|
Project-URL: Homepage, https://github.com/huynguyengl99/chanx
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Management commands for Chanx Channels."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Management commands for Chanx Channels."""
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django management command to generate AsyncAPI schema file.
|
|
3
|
+
|
|
4
|
+
This command generates an AsyncAPI 3.0-compliant schema for your WebSocket API
|
|
5
|
+
without requiring an HTTP request-response flow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from textwrap import dedent
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
14
|
+
from django.test import RequestFactory
|
|
15
|
+
from django.utils.module_loading import import_string
|
|
16
|
+
|
|
17
|
+
from chanx.asyncapi.generator import AsyncAPIGenerator
|
|
18
|
+
from chanx.channels.discovery import DjangoRouteDiscovery
|
|
19
|
+
from chanx.channels.settings import chanx_settings
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
yaml_available = True
|
|
25
|
+
except ImportError: # pragma: no cover
|
|
26
|
+
yaml = cast(ModuleType, None)
|
|
27
|
+
yaml_available = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SchemaGenerationError(CommandError):
|
|
31
|
+
"""Exception raised when schema generation fails."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Command(BaseCommand):
|
|
37
|
+
"""Management command to generate AsyncAPI schema."""
|
|
38
|
+
|
|
39
|
+
help = dedent(
|
|
40
|
+
"""
|
|
41
|
+
Generate an AsyncAPI 3.0-compliant schema for your WebSocket API.
|
|
42
|
+
|
|
43
|
+
This command introspects your Django Channels routing configuration and
|
|
44
|
+
generates a complete AsyncAPI specification file that can be used for
|
|
45
|
+
documentation, code generation, and API testing.
|
|
46
|
+
|
|
47
|
+
The command supports both JSON and YAML output formats and can write
|
|
48
|
+
to a file or stdout.
|
|
49
|
+
|
|
50
|
+
Example usage:
|
|
51
|
+
# Generate YAML schema and write to file
|
|
52
|
+
python manage.py generate_asyncapi_schema --format yaml --file schema.yaml
|
|
53
|
+
|
|
54
|
+
# Generate JSON schema to stdout
|
|
55
|
+
python manage.py generate_asyncapi_schema --format json
|
|
56
|
+
|
|
57
|
+
# Use custom settings and base URL
|
|
58
|
+
python manage.py generate_asyncapi_schema --base-url ws://example.com --format yaml
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def add_arguments(self, parser: Any) -> None:
|
|
63
|
+
"""Add command arguments."""
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--format",
|
|
66
|
+
dest="format",
|
|
67
|
+
choices=["json", "yaml"],
|
|
68
|
+
default="yaml",
|
|
69
|
+
type=str,
|
|
70
|
+
help="Output format (json or yaml)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--file",
|
|
74
|
+
dest="file",
|
|
75
|
+
default=None,
|
|
76
|
+
type=str,
|
|
77
|
+
help="Output file path (if not specified, outputs to stdout)",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--base-url",
|
|
81
|
+
dest="base_url",
|
|
82
|
+
default=None,
|
|
83
|
+
type=str,
|
|
84
|
+
help="WebSocket base URL (e.g., ws://localhost:8000)",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--title",
|
|
88
|
+
dest="title",
|
|
89
|
+
default=None,
|
|
90
|
+
type=str,
|
|
91
|
+
help="API title for AsyncAPI documentation",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--api-version",
|
|
95
|
+
dest="api_version",
|
|
96
|
+
default=None,
|
|
97
|
+
type=str,
|
|
98
|
+
help="API version for AsyncAPI documentation",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--description",
|
|
102
|
+
dest="description",
|
|
103
|
+
default=None,
|
|
104
|
+
type=str,
|
|
105
|
+
help="API description for AsyncAPI documentation",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--discovery-class",
|
|
109
|
+
dest="discovery_class",
|
|
110
|
+
default=None,
|
|
111
|
+
type=str,
|
|
112
|
+
help="Custom route discovery class path",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
|
116
|
+
"""Execute the command."""
|
|
117
|
+
# Check if YAML is required but not available
|
|
118
|
+
if options["format"] == "yaml" and not yaml_available:
|
|
119
|
+
raise SchemaGenerationError(
|
|
120
|
+
"YAML format requested but PyYAML is not installed. "
|
|
121
|
+
"Please install it: pip install PyYAML"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Generate the schema
|
|
126
|
+
schema = self._generate_schema(options)
|
|
127
|
+
|
|
128
|
+
# Render the schema in the requested format
|
|
129
|
+
output = self._render_schema(schema, options["format"])
|
|
130
|
+
|
|
131
|
+
# Write to file or stdout
|
|
132
|
+
if options["file"]:
|
|
133
|
+
with open(options["file"], "wb") as f:
|
|
134
|
+
f.write(output)
|
|
135
|
+
self.stdout.write(
|
|
136
|
+
self.style.SUCCESS(
|
|
137
|
+
f"AsyncAPI schema successfully generated: {options['file']}"
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
self.stdout.write(output.decode())
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise SchemaGenerationError(f"Failed to generate schema: {str(e)}") from e
|
|
145
|
+
|
|
146
|
+
def _generate_schema(self, options: dict[str, Any]) -> dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Generate AsyncAPI schema.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
options: Command options
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
AsyncAPI schema dictionary
|
|
155
|
+
"""
|
|
156
|
+
# Get or create route discovery instance
|
|
157
|
+
if options.get("discovery_class"):
|
|
158
|
+
discovery_class = import_string(options["discovery_class"])
|
|
159
|
+
discovery = discovery_class()
|
|
160
|
+
else:
|
|
161
|
+
discovery = DjangoRouteDiscovery()
|
|
162
|
+
|
|
163
|
+
# Determine base URL
|
|
164
|
+
base_url = options.get("base_url") or self._get_default_base_url()
|
|
165
|
+
|
|
166
|
+
# Discover routes
|
|
167
|
+
routes = discovery.discover_routes(base_url)
|
|
168
|
+
|
|
169
|
+
if not routes:
|
|
170
|
+
self.stderr.write(
|
|
171
|
+
self.style.WARNING(
|
|
172
|
+
"No WebSocket routes found. Make sure your ASGI application "
|
|
173
|
+
"is properly configured."
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Create a fake request for settings that need it
|
|
178
|
+
request_factory = RequestFactory()
|
|
179
|
+
request_factory.get("/")
|
|
180
|
+
|
|
181
|
+
# Parse base URL for server configuration
|
|
182
|
+
server_url, server_protocol = self._parse_base_url(base_url)
|
|
183
|
+
|
|
184
|
+
# Use custom settings or fall back to defaults
|
|
185
|
+
title = options.get("title") or chanx_settings.ASYNCAPI_TITLE
|
|
186
|
+
version = options.get("api_version") or chanx_settings.ASYNCAPI_VERSION
|
|
187
|
+
description = options.get("description") or chanx_settings.ASYNCAPI_DESCRIPTION
|
|
188
|
+
|
|
189
|
+
# Generate AsyncAPI schema using the generator
|
|
190
|
+
generator = AsyncAPIGenerator(
|
|
191
|
+
routes,
|
|
192
|
+
title=title,
|
|
193
|
+
version=version,
|
|
194
|
+
description=description,
|
|
195
|
+
server_url=server_url,
|
|
196
|
+
server_protocol=server_protocol,
|
|
197
|
+
camelize=chanx_settings.CAMELIZE,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return generator.generate()
|
|
201
|
+
|
|
202
|
+
def _get_default_base_url(self) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Get default base URL from settings or use localhost.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Default WebSocket base URL
|
|
208
|
+
"""
|
|
209
|
+
if chanx_settings.WEBSOCKET_BASE_URL:
|
|
210
|
+
return chanx_settings.WEBSOCKET_BASE_URL
|
|
211
|
+
return "ws://localhost:8000"
|
|
212
|
+
|
|
213
|
+
def _parse_base_url(self, base_url: str) -> tuple[str, str]:
|
|
214
|
+
"""
|
|
215
|
+
Parse base URL into server URL and protocol.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
base_url: Full WebSocket URL (e.g., ws://localhost:8000)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Tuple of (server_url, protocol)
|
|
222
|
+
"""
|
|
223
|
+
# Extract protocol (ws or wss)
|
|
224
|
+
if base_url.startswith("wss://"):
|
|
225
|
+
protocol = "wss"
|
|
226
|
+
server_url = base_url[6:] # Remove 'wss://'
|
|
227
|
+
elif base_url.startswith("ws://"):
|
|
228
|
+
protocol = "ws"
|
|
229
|
+
server_url = base_url[5:] # Remove 'ws://'
|
|
230
|
+
else:
|
|
231
|
+
# Default to ws if no protocol specified
|
|
232
|
+
protocol = "ws"
|
|
233
|
+
server_url = base_url
|
|
234
|
+
|
|
235
|
+
return server_url, protocol
|
|
236
|
+
|
|
237
|
+
def _render_schema(self, schema: dict[str, Any], format_type: str) -> bytes:
|
|
238
|
+
"""
|
|
239
|
+
Render schema in the requested format.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
schema: AsyncAPI schema dictionary
|
|
243
|
+
format_type: Output format ('json' or 'yaml')
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Rendered schema as bytes
|
|
247
|
+
"""
|
|
248
|
+
if format_type == "yaml":
|
|
249
|
+
yaml_content = yaml.dump(
|
|
250
|
+
schema,
|
|
251
|
+
default_flow_style=False,
|
|
252
|
+
sort_keys=False,
|
|
253
|
+
indent=2,
|
|
254
|
+
allow_unicode=True,
|
|
255
|
+
)
|
|
256
|
+
return yaml_content.encode("utf-8")
|
|
257
|
+
else:
|
|
258
|
+
json_content = json.dumps(schema, indent=2, ensure_ascii=False)
|
|
259
|
+
return json_content.encode("utf-8")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|