nitro-framework 0.2.0__tar.gz → 0.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.
Files changed (142) hide show
  1. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/Cargo.lock +5 -5
  2. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/Cargo.toml +4 -4
  3. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/PKG-INFO +1 -1
  4. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/websocket.rs +60 -0
  5. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/protocol.rs +12 -1
  6. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/__init__.py +1 -1
  7. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/__init__.py +40 -1
  8. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/pyproject.toml +1 -1
  9. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/LICENSE-APACHE +0 -0
  10. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/LICENSE-MIT +0 -0
  11. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/README.md +0 -0
  12. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/Cargo.toml +0 -0
  13. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/LICENSE-APACHE +0 -0
  14. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/LICENSE-MIT +0 -0
  15. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/src/channel.rs +0 -0
  16. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/src/codec.rs +0 -0
  17. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/src/intercom_core.rs +0 -0
  18. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/src/redis.rs +0 -0
  19. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/intercom-core/tests/redis_backend.rs +0 -0
  20. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/Cargo.toml +0 -0
  21. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/LICENSE-APACHE +0 -0
  22. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/LICENSE-MIT +0 -0
  23. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/examples/floor.rs +0 -0
  24. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/config.rs +0 -0
  25. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/disconnect.rs +0 -0
  26. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/files.rs +0 -0
  27. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/headers.rs +0 -0
  28. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/hosts.rs +0 -0
  29. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/lifecycle/drain.rs +0 -0
  30. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/lifecycle/signals.rs +0 -0
  31. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/lifecycle.rs +0 -0
  32. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/nitro_core.rs +0 -0
  33. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/router/matcher.rs +0 -0
  34. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/router/route.rs +0 -0
  35. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/router.rs +0 -0
  36. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/streaming.rs +0 -0
  37. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/transport/accept.rs +0 -0
  38. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/transport/connection.rs +0 -0
  39. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/transport/quic.rs +0 -0
  40. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/transport/tls.rs +0 -0
  41. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/transport.rs +0 -0
  42. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/src/webtransport.rs +0 -0
  43. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/tests/observability.rs +0 -0
  44. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/tests/serving.rs +0 -0
  45. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/tests/signals.rs +0 -0
  46. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-core/tests/webtransport.rs +0 -0
  47. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/Cargo.toml +0 -0
  48. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/LICENSE-APACHE +0 -0
  49. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/LICENSE-MIT +0 -0
  50. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/src/exporter.rs +0 -0
  51. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/src/metrics.rs +0 -0
  52. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-observability/src/nitro_observability.rs +0 -0
  53. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/Cargo.toml +0 -0
  54. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/LICENSE-APACHE +0 -0
  55. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/LICENSE-MIT +0 -0
  56. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/config.rs +0 -0
  57. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/dispatch.rs +0 -0
  58. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/handoff.rs +0 -0
  59. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/headers.rs +0 -0
  60. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/intercom_bridge.rs +0 -0
  61. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/lifecycle.rs +0 -0
  62. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/nitro_py.rs +0 -0
  63. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/scope.rs +0 -0
  64. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/crates/nitro-py/src/server.rs +0 -0
  65. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/app.py +0 -0
  66. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/__init__.py +0 -0
  67. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/backends/__init__.py +0 -0
  68. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/backends/memcached.py +0 -0
  69. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/backends/memory.py +0 -0
  70. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/backends/redis.py +0 -0
  71. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/base.py +0 -0
  72. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cache/handler.py +0 -0
  73. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/__main__.py +0 -0
  74. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/commands/__init__.py +0 -0
  75. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/commands/check.py +0 -0
  76. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/commands/shell.py +0 -0
  77. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/commands/static.py +0 -0
  78. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/commands/version.py +0 -0
  79. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/cli/serve.py +0 -0
  80. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/di/__init__.py +0 -0
  81. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/endpoints.py +0 -0
  82. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/intercom/__init__.py +0 -0
  83. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/intercom/backends.py +0 -0
  84. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/log.py +0 -0
  85. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/__init__.py +0 -0
  86. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/__init__.py +0 -0
  87. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/base.py +0 -0
  88. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/console.py +0 -0
  89. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/oauth_smtp.py +0 -0
  90. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/sendgrid.py +0 -0
  91. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/ses.py +0 -0
  92. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/backends/smtp.py +0 -0
  93. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/message.py +0 -0
  94. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/mail/utils.py +0 -0
  95. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/middleware/__init__.py +0 -0
  96. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/middleware/base.py +0 -0
  97. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/middleware/common.py +0 -0
  98. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/middleware/stack.py +0 -0
  99. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/protocols/__init__.py +0 -0
  100. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/protocols/exceptions.py +0 -0
  101. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/protocols/http.py +0 -0
  102. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/protocols/websocket.py +0 -0
  103. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/protocols/webtransport.py +0 -0
  104. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/reload.py +0 -0
  105. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/__init__.py +0 -0
  106. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/converters.py +0 -0
  107. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/mount.py +0 -0
  108. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/parameters.py +0 -0
  109. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/patterns.py +0 -0
  110. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/reverse.py +0 -0
  111. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/routing/router.py +0 -0
  112. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/sessions.py +0 -0
  113. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/settings/__init__.py +0 -0
  114. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/settings/defaults.py +0 -0
  115. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/shortcuts.py +0 -0
  116. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/staticfiles.py +0 -0
  117. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/__init__.py +0 -0
  118. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/backends/__init__.py +0 -0
  119. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/backends/azure.py +0 -0
  120. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/backends/filesystem.py +0 -0
  121. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/backends/memory.py +0 -0
  122. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/backends/s3.py +0 -0
  123. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/base.py +0 -0
  124. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/storage/handler.py +0 -0
  125. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/templates/__init__.py +0 -0
  126. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/templates/engine.py +0 -0
  127. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/templates/exceptions.py +0 -0
  128. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/templates/templates.py +0 -0
  129. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/testing.py +0 -0
  130. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/__init__.py +0 -0
  131. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/archives.py +0 -0
  132. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/content.py +0 -0
  133. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/crypto.py +0 -0
  134. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/datetime.py +0 -0
  135. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/decorators.py +0 -0
  136. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/http.py +0 -0
  137. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/json.py +0 -0
  138. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/utils/modules.py +0 -0
  139. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/views/__init__.py +0 -0
  140. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/views/debug.py +0 -0
  141. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/views/templates/error_404.html +0 -0
  142. {nitro_framework-0.2.0 → nitro_framework-0.3.0}/nitro/views/templates/error_500.html +0 -0
@@ -826,7 +826,7 @@ dependencies = [
826
826
 
827
827
  [[package]]
828
828
  name = "intercom-core"
829
- version = "0.2.0"
829
+ version = "0.3.0"
830
830
  dependencies = [
831
831
  "bytes",
832
832
  "futures-util",
@@ -841,7 +841,7 @@ dependencies = [
841
841
 
842
842
  [[package]]
843
843
  name = "intercom-py"
844
- version = "0.2.0"
844
+ version = "0.3.0"
845
845
  dependencies = [
846
846
  "bytes",
847
847
  "futures-util",
@@ -1029,7 +1029,7 @@ dependencies = [
1029
1029
 
1030
1030
  [[package]]
1031
1031
  name = "nitro-core"
1032
- version = "0.2.0"
1032
+ version = "0.3.0"
1033
1033
  dependencies = [
1034
1034
  "base64 0.23.1",
1035
1035
  "bytes",
@@ -1071,7 +1071,7 @@ dependencies = [
1071
1071
 
1072
1072
  [[package]]
1073
1073
  name = "nitro-observability"
1074
- version = "0.2.0"
1074
+ version = "0.3.0"
1075
1075
  dependencies = [
1076
1076
  "bytes",
1077
1077
  "futures-util",
@@ -1089,7 +1089,7 @@ dependencies = [
1089
1089
 
1090
1090
  [[package]]
1091
1091
  name = "nitro-py"
1092
- version = "0.2.0"
1092
+ version = "0.3.0"
1093
1093
  dependencies = [
1094
1094
  "bytes",
1095
1095
  "futures-util",
@@ -5,14 +5,14 @@ members = ["crates/nitro-core", "crates/nitro-observability", "crates/nitro-py",
5
5
  [workspace.package]
6
6
  edition = "2024"
7
7
  license = "MIT OR Apache-2.0"
8
- version = "0.2.0"
8
+ version = "0.3.0"
9
9
  authors = ["Mario Ritzerfeld <oss@jaldis.com>"]
10
10
  repository = "https://github.com/jaldisdev/nitro"
11
11
 
12
12
  [workspace.dependencies]
13
- nitro-core = { path = "crates/nitro-core", version = "0.2.0" }
14
- nitro-observability = { path = "crates/nitro-observability", version = "0.2.0" }
15
- intercom-core = { path = "crates/intercom-core", version = "0.2.0" }
13
+ nitro-core = { path = "crates/nitro-core", version = "0.3.0" }
14
+ nitro-observability = { path = "crates/nitro-observability", version = "0.3.0" }
15
+ intercom-core = { path = "crates/intercom-core", version = "0.3.0" }
16
16
 
17
17
  # Async runtime
18
18
  tokio = { version = "1.53.1", features = ["full"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nitro-framework
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: AsyncIO
6
6
  Classifier: Intended Audience :: Developers
@@ -384,11 +384,29 @@ impl WebSocketReceiver {
384
384
  }
385
385
  }
386
386
 
387
+ /// A peer that goes away without closing is an ended connection, not a failure:
388
+ /// a browser tab closing, a reloading server, a dropped network. Reporting those
389
+ /// as transport failures would make every handler recognise them by their text.
387
390
  fn transport_error(error: tokio_tungstenite::tungstenite::Error) -> WebSocketError {
391
+ use std::io::ErrorKind;
392
+
388
393
  use tokio_tungstenite::tungstenite::Error;
394
+ use tokio_tungstenite::tungstenite::error::ProtocolError;
389
395
 
390
396
  match error {
391
397
  Error::ConnectionClosed | Error::AlreadyClosed => WebSocketError::Closed,
398
+ Error::Protocol(ProtocolError::ResetWithoutClosingHandshake) => WebSocketError::Closed,
399
+ Error::Io(failure)
400
+ if matches!(
401
+ failure.kind(),
402
+ ErrorKind::ConnectionReset
403
+ | ErrorKind::ConnectionAborted
404
+ | ErrorKind::BrokenPipe
405
+ | ErrorKind::UnexpectedEof
406
+ ) =>
407
+ {
408
+ WebSocketError::Closed
409
+ }
392
410
  other => WebSocketError::Transport(other.to_string()),
393
411
  }
394
412
  }
@@ -413,6 +431,48 @@ mod tests {
413
431
  headers
414
432
  }
415
433
 
434
+ #[test]
435
+ fn a_peer_that_vanishes_reads_as_a_closed_connection() {
436
+ use std::io::ErrorKind;
437
+
438
+ use tokio_tungstenite::tungstenite::Error;
439
+ use tokio_tungstenite::tungstenite::error::ProtocolError;
440
+
441
+ let vanished = [
442
+ Error::ConnectionClosed,
443
+ Error::AlreadyClosed,
444
+ Error::Protocol(ProtocolError::ResetWithoutClosingHandshake),
445
+ Error::Io(ErrorKind::ConnectionReset.into()),
446
+ Error::Io(ErrorKind::BrokenPipe.into()),
447
+ Error::Io(ErrorKind::UnexpectedEof.into()),
448
+ ];
449
+
450
+ for error in vanished {
451
+ let reported = transport_error(error);
452
+ assert!(
453
+ matches!(reported, WebSocketError::Closed),
454
+ "expected a closed connection, got {reported:?}"
455
+ );
456
+ }
457
+ }
458
+
459
+ #[test]
460
+ fn a_real_transport_failure_is_still_reported_as_one() {
461
+ use std::io::ErrorKind;
462
+
463
+ use tokio_tungstenite::tungstenite::Error;
464
+ use tokio_tungstenite::tungstenite::error::ProtocolError;
465
+
466
+ assert!(matches!(
467
+ transport_error(Error::Io(ErrorKind::PermissionDenied.into())),
468
+ WebSocketError::Transport(_)
469
+ ));
470
+ assert!(matches!(
471
+ transport_error(Error::Protocol(ProtocolError::WrongHttpMethod)),
472
+ WebSocketError::Transport(_)
473
+ ));
474
+ }
475
+
416
476
  #[test]
417
477
  fn a_complete_upgrade_request_is_recognised() {
418
478
  assert!(is_upgrade_request(&upgrade_headers()));
@@ -610,7 +610,18 @@ async fn next_message(
610
610
  return Ok(None);
611
611
  };
612
612
 
613
- match message.map_err(socket_error)? {
613
+ let message = match message {
614
+ Ok(message) => message,
615
+ // The connection ending is the end of the stream, however it ended
616
+ Err(WebSocketError::Closed) => {
617
+ *guard = None;
618
+ phase.store(PHASE_CLOSED, Ordering::Release);
619
+ return Ok(None);
620
+ }
621
+ Err(failure) => return Err(socket_error(failure)),
622
+ };
623
+
624
+ match message {
614
625
  WebSocketMessage::Text(text) => {
615
626
  return Python::attach(|python| {
616
627
  Ok(Some(text.into_pyobject(python)?.into_any().unbind()))
@@ -21,6 +21,6 @@
21
21
 
22
22
  from nitro.app import Nitro
23
23
 
24
- __version__ = "0.2.0"
24
+ __version__ = "0.3.0"
25
25
 
26
26
  __all__ = ["Nitro", "__version__"]
@@ -71,6 +71,32 @@ class RootCommand(click.Group):
71
71
  }
72
72
  settings.update(kwargs.pop("context_settings", None) or {})
73
73
  super().__init__(*args, context_settings=settings, **kwargs)
74
+ self._project_commands_loaded = False
75
+
76
+ def get_command(self, context: click.Context, name: str) -> click.Command | None:
77
+ command = super().get_command(context, name)
78
+ # An application specifier always carries a colon, and naming one is
79
+ # no reason to import the project's commands.
80
+ if command is None and ":" not in name and self._load_project_commands():
81
+ command = super().get_command(context, name)
82
+ return command
83
+
84
+ def list_commands(self, context: click.Context) -> list[str]:
85
+ self._load_project_commands()
86
+ return super().list_commands(context)
87
+
88
+ def _load_project_commands(self) -> bool:
89
+ """Register the project's commands the first time one could be meant.
90
+
91
+ Importing them imports the project, which neither a built-in command
92
+ nor serving has any use for — and a reload supervisor would pay for it
93
+ on every start. Returns whether this call was the one that loaded them.
94
+ """
95
+ if self._project_commands_loaded:
96
+ return False
97
+ self._project_commands_loaded = True
98
+ load_project_commands(self)
99
+ return True
74
100
 
75
101
  def resolve_command(
76
102
  self, context: click.Context, args: list[str]
@@ -165,7 +191,21 @@ def _commands_in_module(module: object) -> list[click.Command]:
165
191
 
166
192
 
167
193
  def register_commands(group: click.Group, package_name: str) -> None:
194
+ """Add the commands of `package_name` to `group`, never replacing one.
195
+
196
+ Project commands are registered only once one is asked for, so a built-in
197
+ has already answered to its name by then; letting a project command take
198
+ it over would make the name mean one thing or the other depending on what
199
+ was run first.
200
+ """
168
201
  for command in commands_in(package_name):
202
+ if command.name in group.commands:
203
+ logger.warning(
204
+ "command %r from %r is already taken; keeping the one registered first",
205
+ command.name,
206
+ package_name,
207
+ )
208
+ continue
169
209
  group.add_command(command)
170
210
 
171
211
 
@@ -200,5 +240,4 @@ def main() -> None:
200
240
 
201
241
  configure_logging()
202
242
  register_commands(cli, BUILTIN_COMMAND_PACKAGE)
203
- load_project_commands(cli)
204
243
  cli()
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "nitro-framework"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Async-first Python web framework with a bundled Rust server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"