nebu 0.1.115__py3-none-any.whl → 0.1.117__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.
@@ -33,11 +33,20 @@ local_namespace: Dict[str, Any] = {} # Namespace for included objects
33
33
  last_load_mtime: float = 0.0
34
34
  entrypoint_abs_path: Optional[str] = None
35
35
 
36
+ # Global health check subprocess
37
+ health_subprocess: Optional[subprocess.Popen] = None
38
+
36
39
  REDIS_CONSUMER_GROUP = os.environ.get("REDIS_CONSUMER_GROUP")
37
40
  REDIS_STREAM = os.environ.get("REDIS_STREAM")
38
41
  NEBU_EXECUTION_MODE = os.environ.get("NEBU_EXECUTION_MODE", "inline").lower()
39
42
  execution_mode = NEBU_EXECUTION_MODE
40
43
 
44
+ # Define health check stream and group names
45
+ REDIS_HEALTH_STREAM = f"{REDIS_STREAM}.health" if REDIS_STREAM else None
46
+ REDIS_HEALTH_CONSUMER_GROUP = (
47
+ f"{REDIS_CONSUMER_GROUP}-health" if REDIS_CONSUMER_GROUP else None
48
+ )
49
+
41
50
  if execution_mode not in ["inline", "subprocess"]:
42
51
  logger.warning(
43
52
  f"Invalid NEBU_EXECUTION_MODE: {NEBU_EXECUTION_MODE}. Must be 'inline' or 'subprocess'. Defaulting to 'inline'."
@@ -328,20 +337,34 @@ socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
328
337
  socket.socket = socks.socksocket
329
338
  logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
330
339
 
331
- # Connect to Redis
332
- try:
333
- # Parse the Redis URL to handle potential credentials or specific DBs if needed
334
- # Although from_url should work now with the patched socket
335
- r = redis.from_url(
336
- REDIS_URL, decode_responses=True
337
- ) # Added decode_responses for convenience
338
- r.ping() # Test connection
339
- redis_info = REDIS_URL.split("@")[-1] if "@" in REDIS_URL else REDIS_URL
340
- logger.info(f"Connected to Redis via SOCKS proxy at {redis_info}")
341
- except Exception as e:
342
- logger.critical(f"Failed to connect to Redis via SOCKS proxy: {e}")
343
- logger.exception("Redis Connection Error Traceback:")
344
- sys.exit(1)
340
+ # Global Redis connection for the main consumer
341
+ r: redis.Redis # Initialized by connect_redis, which sys.exits on failure
342
+
343
+
344
+ # --- Connect to Redis (Main Consumer) ---
345
+ def connect_redis(redis_url: str) -> redis.Redis:
346
+ """Connects to Redis and returns the connection object."""
347
+ try:
348
+ # Parse the Redis URL to handle potential credentials or specific DBs if needed
349
+ # Although from_url should work now with the patched socket
350
+ logger.info(
351
+ f"Attempting to connect to Redis at {redis_url.split('@')[-1] if '@' in redis_url else redis_url}"
352
+ )
353
+ conn = redis.from_url(
354
+ redis_url, decode_responses=True
355
+ ) # Added decode_responses for convenience
356
+ conn.ping() # Test connection
357
+ redis_info = redis_url.split("@")[-1] if "@" in redis_url else redis_url
358
+ logger.info(f"Connected to Redis via SOCKS proxy at {redis_info}")
359
+ return conn
360
+ except Exception as e:
361
+ logger.critical(f"Failed to connect to Redis via SOCKS proxy: {e}")
362
+ logger.exception("Redis Connection Error Traceback:")
363
+ sys.exit(1)
364
+
365
+
366
+ r = connect_redis(REDIS_URL)
367
+
345
368
 
346
369
  # Create consumer group if it doesn't exist
347
370
  try:
@@ -360,6 +383,103 @@ except ResponseError as e:
360
383
  logger.exception("Consumer Group Creation Error Traceback:")
361
384
 
362
385
 
386
+ # --- Health Check Subprocess Management ---
387
+ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
388
+ """Start the health check consumer subprocess."""
389
+ global REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP
390
+
391
+ if not all([REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP]):
392
+ logger.warning(
393
+ "[Consumer] Health check stream not configured. Health consumer subprocess not started."
394
+ )
395
+ return None
396
+
397
+ try:
398
+ # Type assertions to ensure variables are strings before using them
399
+ assert isinstance(REDIS_HEALTH_STREAM, str)
400
+ assert isinstance(REDIS_HEALTH_CONSUMER_GROUP, str)
401
+
402
+ # Prepare environment variables for the subprocess
403
+ health_env = os.environ.copy()
404
+ health_env["REDIS_HEALTH_STREAM"] = REDIS_HEALTH_STREAM
405
+ health_env["REDIS_HEALTH_CONSUMER_GROUP"] = REDIS_HEALTH_CONSUMER_GROUP
406
+
407
+ # Start the health check worker subprocess
408
+ health_cmd = [
409
+ sys.executable,
410
+ "-u", # Force unbuffered stdout/stderr
411
+ "-m",
412
+ "nebu.processors.consumer_health_worker",
413
+ ]
414
+
415
+ process = subprocess.Popen(
416
+ health_cmd,
417
+ stdout=subprocess.PIPE,
418
+ stderr=subprocess.STDOUT, # Combine stderr with stdout
419
+ text=True,
420
+ encoding="utf-8",
421
+ env=health_env,
422
+ bufsize=1, # Line buffered
423
+ )
424
+
425
+ logger.info(
426
+ f"[Consumer] Health check subprocess started with PID {process.pid}"
427
+ )
428
+ return process
429
+
430
+ except Exception as e:
431
+ logger.error(f"[Consumer] Failed to start health check subprocess: {e}")
432
+ logger.exception("Health Subprocess Start Error Traceback:")
433
+ return None
434
+
435
+
436
+ def monitor_health_subprocess(process: subprocess.Popen) -> None:
437
+ """Monitor the health check subprocess and log its output."""
438
+ try:
439
+ # Read output from the subprocess
440
+ if process.stdout:
441
+ for line in iter(process.stdout.readline, ""):
442
+ logger.info(f"[HealthSubprocess] {line.strip()}")
443
+ process.stdout.close() if process.stdout else None
444
+ except Exception as e:
445
+ logger.error(f"[Consumer] Error monitoring health subprocess: {e}")
446
+
447
+
448
+ def check_health_subprocess() -> bool:
449
+ """Check if the health subprocess is still running and restart if needed."""
450
+ global health_subprocess
451
+
452
+ if health_subprocess is None:
453
+ return False
454
+
455
+ # Check if process is still running
456
+ if health_subprocess.poll() is None:
457
+ return True # Still running
458
+
459
+ # Process has exited
460
+ exit_code = health_subprocess.returncode
461
+ logger.warning(
462
+ f"[Consumer] Health subprocess exited with code {exit_code}. Restarting..."
463
+ )
464
+
465
+ # Start a new health subprocess
466
+ health_subprocess = start_health_check_subprocess()
467
+
468
+ if health_subprocess:
469
+ # Start monitoring thread for the new subprocess
470
+ monitor_thread = threading.Thread(
471
+ target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
472
+ )
473
+ monitor_thread.start()
474
+ logger.info(
475
+ "[Consumer] Health subprocess restarted and monitoring thread started."
476
+ )
477
+ return True
478
+ else:
479
+ logger.error("[Consumer] Failed to restart health subprocess.")
480
+ return False
481
+
482
+
363
483
  # Function to process messages
364
484
  def process_message(message_id: str, message_data: Dict[str, str]) -> None:
365
485
  # Access the globally managed user code elements
@@ -1088,11 +1208,35 @@ logger.info(
1088
1208
  f"[Consumer] Hot code reloading is {'DISABLED' if disable_hot_reload else 'ENABLED'}."
1089
1209
  )
1090
1210
 
1211
+ # Start the health check consumer subprocess
1212
+ if REDIS_HEALTH_STREAM and REDIS_HEALTH_CONSUMER_GROUP:
1213
+ health_subprocess = start_health_check_subprocess()
1214
+ if health_subprocess:
1215
+ # Start monitoring thread for subprocess output
1216
+ monitor_thread = threading.Thread(
1217
+ target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
1218
+ )
1219
+ monitor_thread.start()
1220
+ logger.info(
1221
+ f"[Consumer] Health check subprocess for {REDIS_HEALTH_STREAM} started and monitoring thread started."
1222
+ )
1223
+ else:
1224
+ logger.error("[Consumer] Failed to start health check subprocess.")
1225
+ else:
1226
+ logger.warning(
1227
+ "[Consumer] Health check stream not configured. Health consumer subprocess not started."
1228
+ )
1229
+
1091
1230
  try:
1092
1231
  while True:
1093
1232
  logger.debug(
1094
1233
  f"[{datetime.now(timezone.utc).isoformat()}] --- Top of main loop ---"
1095
1234
  ) # Added log
1235
+
1236
+ # --- Check Health Subprocess Status ---
1237
+ if health_subprocess:
1238
+ check_health_subprocess()
1239
+
1096
1240
  # --- Check for Code Updates ---
1097
1241
  if not disable_hot_reload:
1098
1242
  logger.debug(
@@ -1356,18 +1500,21 @@ except ConnectionError as e:
1356
1500
  # Attempt to reconnect explicitly
1357
1501
  try:
1358
1502
  logger.info("Attempting Redis reconnection...")
1359
- # Close existing potentially broken connection? `r.close()` if available
1360
- r = redis.from_url(REDIS_URL, decode_responses=True)
1361
- r.ping()
1503
+ # Close existing potentially broken connection?
1504
+ if r: # Check if r was initialized
1505
+ try:
1506
+ r.close()
1507
+ except Exception:
1508
+ pass # Ignore errors during close
1509
+ r = connect_redis(REDIS_URL) # connect_redis will sys.exit on failure
1362
1510
  logger.info("Reconnected to Redis.")
1363
- except Exception as recon_e:
1511
+ except Exception as recon_e: # Should not be reached if connect_redis exits
1364
1512
  logger.error(f"Failed to reconnect to Redis: {recon_e}")
1365
- # Keep waiting
1366
1513
 
1367
1514
  except ResponseError as e:
1368
1515
  logger.error(f"Redis command error: {e}")
1369
1516
  # Should we exit or retry?
1370
- if "NOGROUP" in str(e):
1517
+ if r and "NOGROUP" in str(e): # Check if r is initialized
1371
1518
  logger.critical("Consumer group seems to have disappeared. Exiting.")
1372
1519
  sys.exit(1)
1373
1520
  time.sleep(1)
@@ -1379,4 +1526,15 @@ except Exception as e:
1379
1526
 
1380
1527
  finally:
1381
1528
  logger.info("Consumer loop exited.")
1382
- # Any other cleanup needed?
1529
+ # Cleanup health subprocess
1530
+ if health_subprocess and health_subprocess.poll() is None:
1531
+ logger.info("[Consumer] Terminating health check subprocess...")
1532
+ health_subprocess.terminate()
1533
+ try:
1534
+ health_subprocess.wait(timeout=5)
1535
+ except subprocess.TimeoutExpired:
1536
+ logger.warning(
1537
+ "[Consumer] Health subprocess did not terminate gracefully, killing it."
1538
+ )
1539
+ health_subprocess.kill()
1540
+ logger.info("[Consumer] Health subprocess cleanup complete.")
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import logging
4
+ import os
5
+ import socket
6
+ import sys
7
+ import time
8
+ from typing import Dict, List, Optional, Tuple, cast
9
+
10
+ import redis
11
+ import socks
12
+ from redis import ConnectionError, ResponseError
13
+ from redis.exceptions import TimeoutError as RedisTimeoutError
14
+
15
+
16
+ def setup_health_logging():
17
+ """Set up logging for the health check worker to write to a dedicated file."""
18
+ # Create logs directory if it doesn't exist
19
+ log_dir = os.path.join(os.getcwd(), "logs")
20
+ os.makedirs(log_dir, exist_ok=True)
21
+
22
+ # Create log file path with timestamp
23
+ log_file = os.path.join(log_dir, f"health_consumer_{os.getpid()}.log")
24
+
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29
+ handlers=[
30
+ logging.FileHandler(log_file),
31
+ logging.StreamHandler(
32
+ sys.stdout
33
+ ), # Also log to stdout for subprocess monitoring
34
+ ],
35
+ )
36
+
37
+ logger = logging.getLogger("HealthConsumer")
38
+ logger.info(f"Health check worker started. Logging to: {log_file}")
39
+ return logger
40
+
41
+
42
+ def process_health_check_message(
43
+ message_id: str,
44
+ message_data: Dict[str, str],
45
+ redis_conn: redis.Redis,
46
+ logger: logging.Logger,
47
+ health_stream: str,
48
+ health_group: str,
49
+ ) -> None:
50
+ """Processes a single health check message."""
51
+ logger.info(f"Processing health check message {message_id}: {message_data}")
52
+
53
+ # Parse the message if it contains JSON data
54
+ try:
55
+ if "data" in message_data:
56
+ data = json.loads(message_data["data"])
57
+ logger.info(f"Health check data: {data}")
58
+ except (json.JSONDecodeError, KeyError) as e:
59
+ logger.warning(f"Could not parse health check message data: {e}")
60
+
61
+ # You could add more logic here, e.g., update an internal health status,
62
+ # send a response, perform actual health checks, etc.
63
+
64
+ # Acknowledge the health check message
65
+ try:
66
+ redis_conn.xack(health_stream, health_group, message_id)
67
+ logger.info(f"Acknowledged health check message {message_id}")
68
+ except Exception as e_ack:
69
+ logger.error(
70
+ f"Failed to acknowledge health check message {message_id}: {e_ack}"
71
+ )
72
+
73
+
74
+ def main():
75
+ """Main function for the health check consumer subprocess."""
76
+ logger = setup_health_logging()
77
+
78
+ # Get environment variables
79
+ redis_url = os.environ.get("REDIS_URL")
80
+ health_stream = os.environ.get("REDIS_HEALTH_STREAM")
81
+ health_group = os.environ.get("REDIS_HEALTH_CONSUMER_GROUP")
82
+
83
+ if not all([redis_url, health_stream, health_group]):
84
+ logger.error(
85
+ "Missing required environment variables: REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP"
86
+ )
87
+ sys.exit(1)
88
+
89
+ # Type assertions after validation
90
+ assert isinstance(redis_url, str)
91
+ assert isinstance(health_stream, str)
92
+ assert isinstance(health_group, str)
93
+
94
+ logger.info(
95
+ f"Starting health consumer for stream: {health_stream}, group: {health_group}"
96
+ )
97
+
98
+ # Configure SOCKS proxy
99
+ socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
100
+ socket.socket = socks.socksocket
101
+ logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
102
+
103
+ health_redis_conn: Optional[redis.Redis] = None
104
+ health_consumer_name = f"health-consumer-{os.getpid()}-{socket.gethostname()}"
105
+
106
+ while True:
107
+ try:
108
+ if health_redis_conn is None:
109
+ logger.info("Connecting to Redis for health stream...")
110
+ health_redis_conn = redis.from_url(redis_url, decode_responses=True)
111
+ health_redis_conn.ping()
112
+ logger.info("Connected to Redis for health stream.")
113
+
114
+ # Create health consumer group if it doesn't exist
115
+ try:
116
+ health_redis_conn.xgroup_create(
117
+ health_stream, health_group, id="0", mkstream=True
118
+ )
119
+ logger.info(
120
+ f"Created consumer group {health_group} for stream {health_stream}"
121
+ )
122
+ except ResponseError as e_group:
123
+ if "BUSYGROUP" in str(e_group):
124
+ logger.info(f"Consumer group {health_group} already exists.")
125
+ else:
126
+ logger.error(f"Error creating health consumer group: {e_group}")
127
+ time.sleep(5)
128
+ health_redis_conn = None
129
+ continue
130
+ except Exception as e_group_other:
131
+ logger.error(
132
+ f"Unexpected error creating health consumer group: {e_group_other}"
133
+ )
134
+ time.sleep(5)
135
+ health_redis_conn = None
136
+ continue
137
+
138
+ # Read from health stream
139
+ assert health_redis_conn is not None
140
+
141
+ health_streams_arg: Dict[str, object] = {health_stream: ">"}
142
+ raw_messages = health_redis_conn.xreadgroup(
143
+ health_group,
144
+ health_consumer_name,
145
+ health_streams_arg, # type: ignore[arg-type]
146
+ count=1,
147
+ block=5000, # Block for 5 seconds
148
+ )
149
+
150
+ if raw_messages:
151
+ # Cast to expected type for decode_responses=True
152
+ messages = cast(
153
+ List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], raw_messages
154
+ )
155
+ for _stream_name, stream_messages in messages:
156
+ for message_id, message_data in stream_messages:
157
+ process_health_check_message(
158
+ message_id,
159
+ message_data,
160
+ health_redis_conn,
161
+ logger,
162
+ health_stream,
163
+ health_group,
164
+ )
165
+
166
+ except (ConnectionError, RedisTimeoutError, TimeoutError) as e_conn:
167
+ logger.error(f"Redis connection error: {e_conn}. Reconnecting in 5s...")
168
+ if health_redis_conn:
169
+ try:
170
+ health_redis_conn.close()
171
+ except Exception:
172
+ pass
173
+ health_redis_conn = None
174
+ time.sleep(5)
175
+
176
+ except ResponseError as e_resp:
177
+ logger.error(f"Redis response error: {e_resp}")
178
+ if "NOGROUP" in str(e_resp):
179
+ logger.warning(
180
+ "Health consumer group disappeared. Attempting to recreate..."
181
+ )
182
+ if health_redis_conn:
183
+ try:
184
+ health_redis_conn.close()
185
+ except Exception:
186
+ pass
187
+ health_redis_conn = None
188
+ elif "UNBLOCKED" in str(e_resp):
189
+ logger.info(
190
+ "XREADGROUP unblocked, connection might have been closed. Reconnecting."
191
+ )
192
+ if health_redis_conn:
193
+ try:
194
+ health_redis_conn.close()
195
+ except Exception:
196
+ pass
197
+ health_redis_conn = None
198
+ time.sleep(1)
199
+ else:
200
+ time.sleep(5)
201
+
202
+ except KeyboardInterrupt:
203
+ logger.info("Received interrupt signal. Shutting down health consumer...")
204
+ break
205
+
206
+ except Exception as e:
207
+ logger.error(f"Unexpected error in health check consumer: {e}")
208
+ logger.exception("Traceback:")
209
+ time.sleep(5)
210
+
211
+ # Cleanup
212
+ if health_redis_conn:
213
+ try:
214
+ health_redis_conn.close()
215
+ except Exception:
216
+ pass
217
+
218
+ logger.info("Health check consumer shutdown complete.")
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()
@@ -802,7 +802,13 @@ def processor(
802
802
  )
803
803
  origin = get_origin(param_type) if param_type else None
804
804
  args = get_args(param_type) if param_type else tuple()
805
- logger.debug(f"Decorator: get_origin result: {origin}, get_args result: {args}")
805
+ logger.debug(
806
+ f"Decorator: For param_type '{param_type_str_repr}': origin = {origin!s}, args = {args!s}"
807
+ ) # More detailed log
808
+ print(
809
+ f"Decorator: For param_type '{param_type_str_repr}': origin = {origin!s}, args = {args!s}"
810
+ ) # More detailed log
811
+
806
812
  is_stream_message = False
807
813
  content_type = None
808
814
  content_type_name_from_regex = None # Store regex result here
@@ -311,6 +311,13 @@ class Processor(Generic[InputType, OutputType]):
311
311
  )
312
312
  response.raise_for_status()
313
313
  raw_response_json = response.json()
314
+
315
+ if "error" in raw_response_json:
316
+ raise Exception(raw_response_json["error"])
317
+
318
+ if "status" in raw_response_json:
319
+ return raw_response_json
320
+
314
321
  raw_content = raw_response_json.get("content")
315
322
  logger.debug(f">>> Raw content: {raw_content}")
316
323
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.115
3
+ Version: 0.1.117
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -13,16 +13,17 @@ nebu/containers/container.py,sha256=Mrh_gvMsTvDkj3CwpqIPzJ72IMw0gQIg64y548vq0yg,
13
13
  nebu/containers/models.py,sha256=0j6NGy4yto-enRDh_4JH_ZTbHrLdSpuMOqNQPnIrwC4,6815
14
14
  nebu/namespaces/models.py,sha256=EqUOpzhVBhvJw2P92ONDUbIgC31M9jMmcaG5vyOrsWg,497
15
15
  nebu/namespaces/namespace.py,sha256=oeZyGqsIGIrppyjif1ZONsdTmqRgd9oSLFE1BChXTTE,5247
16
- nebu/processors/consumer.py,sha256=9WapzBTPuXRunH-vjPerTlGZy__hn_d4m13l1ajebY8,62732
16
+ nebu/processors/consumer.py,sha256=18p3QKa9AgVdTv-HH3Iu7cwXTw1pjCy5pKuYqYt0Ndc,68524
17
+ nebu/processors/consumer_health_worker.py,sha256=4tS2vyd71tXMCy93Ae_TQi_ESaZVLPO16f9Gf4Eavcg,7946
17
18
  nebu/processors/consumer_process_worker.py,sha256=h--eNFKaLbUayxn88mB8oGGdrU2liE1dnwm_TPlewX8,36960
18
- nebu/processors/decorate.py,sha256=wnos4w7htBUSOnNVyhY7NQgUuz9d2_-Kre5H99UiFpw,61089
19
+ nebu/processors/decorate.py,sha256=5p9pQrk_H8-Fj0UjsgSVCYx7Jk7KFuhMZtNhkKvpmkQ,61306
19
20
  nebu/processors/default.py,sha256=cy4ETMdbdRGkrvbYec1o60h7mGDlGN5JsuUph0ENtDU,364
20
21
  nebu/processors/models.py,sha256=g4B1t6Rgoy-NUEHBLeQc0EENzHXLDlWSio8Muv7cTDU,4093
21
- nebu/processors/processor.py,sha256=fMXzodTe2qWnoCBry9nLsHLMEI6FxOOmcsR9DbuY_W0,24574
22
+ nebu/processors/processor.py,sha256=oi9QdpvZV91UzVHz63pBFQw9-BeY7xvtJkJblFF_xew,24753
22
23
  nebu/redis/models.py,sha256=coPovAcVXnOU1Xh_fpJL4PO3QctgK9nBe5QYoqEcnxg,1230
23
24
  nebu/services/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- nebu-0.1.115.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
- nebu-0.1.115.dist-info/METADATA,sha256=tud697lMA-FHCANFaBZKT20hTCyIxRMMQT3ZNdc0tQk,1798
26
- nebu-0.1.115.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
27
- nebu-0.1.115.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
28
- nebu-0.1.115.dist-info/RECORD,,
25
+ nebu-0.1.117.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
+ nebu-0.1.117.dist-info/METADATA,sha256=T-j8N4SlzlgRjOk8QvUCc6sCUGvdSBllPd45bEghRUw,1798
27
+ nebu-0.1.117.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
28
+ nebu-0.1.117.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
29
+ nebu-0.1.117.dist-info/RECORD,,
File without changes