sunholo 0.144.2__py3-none-any.whl → 0.144.4__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.
@@ -113,31 +113,92 @@ class VACRoutesFastAPI:
113
113
 
114
114
  ## Basic Usage
115
115
 
116
+ ### Simplified Setup (Recommended)
117
+
118
+ Use the helper method for automatic lifespan management:
119
+
116
120
  ```python
117
- from fastapi import FastAPI
118
121
  from sunholo.agents.fastapi import VACRoutesFastAPI
119
122
 
120
- app = FastAPI()
121
-
122
123
  async def my_stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
123
124
  # Your streaming VAC logic here
124
125
  # Use callback.async_on_llm_new_token(token) for streaming
125
- # Return final result with sources
126
126
  return {"answer": "Response", "sources": []}
127
127
 
128
- # Create VAC routes with MCP server enabled
128
+ # Single call sets up everything with MCP server and proper lifespan management
129
+ app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(
130
+ title="My VAC Application",
131
+ stream_interpreter=my_stream_interpreter
132
+ # MCP server is automatically enabled when using this method
133
+ )
134
+
135
+ # Add custom endpoints if needed
136
+ @app.get("/custom")
137
+ async def custom_endpoint():
138
+ return {"message": "Hello"}
139
+
140
+ # Run the app
141
+ if __name__ == "__main__":
142
+ import uvicorn
143
+ uvicorn.run(app, host="0.0.0.0", port=8000)
144
+ ```
145
+
146
+ ### Manual Setup (Advanced)
147
+
148
+ For more control over lifespan management:
149
+
150
+ ```python
151
+ from contextlib import asynccontextmanager
152
+ from fastapi import FastAPI
153
+ from sunholo.agents.fastapi import VACRoutesFastAPI
154
+
155
+ async def my_stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
156
+ return {"answer": "Response", "sources": []}
157
+
158
+ # Define your app's lifespan
159
+ @asynccontextmanager
160
+ async def app_lifespan(app: FastAPI):
161
+ print("Starting up...")
162
+ yield
163
+ print("Shutting down...")
164
+
165
+ # Create temp app to get MCP lifespan
166
+ temp_app = FastAPI()
167
+ vac_routes_temp = VACRoutesFastAPI(
168
+ temp_app,
169
+ stream_interpreter=my_stream_interpreter,
170
+ enable_mcp_server=True
171
+ )
172
+
173
+ # Get MCP lifespan
174
+ mcp_lifespan = vac_routes_temp.get_mcp_lifespan()
175
+
176
+ # Combine lifespans
177
+ @asynccontextmanager
178
+ async def combined_lifespan(app: FastAPI):
179
+ async with app_lifespan(app):
180
+ if mcp_lifespan:
181
+ async with mcp_lifespan(app):
182
+ yield
183
+ else:
184
+ yield
185
+
186
+ # Create app with combined lifespan
187
+ app = FastAPI(title="My VAC Application", lifespan=combined_lifespan)
188
+
189
+ # Initialize VAC routes
129
190
  vac_routes = VACRoutesFastAPI(
130
191
  app=app,
131
192
  stream_interpreter=my_stream_interpreter,
132
- enable_mcp_server=True # Enable MCP server for Claude Desktop/Code
193
+ enable_mcp_server=True
133
194
  )
134
-
135
- # Your FastAPI app now includes:
136
- # - All VAC endpoints
137
- # - MCP server at /mcp (for Claude Desktop/Code to connect)
138
- # - Built-in VAC tools: vac_stream, vac_query, list_available_vacs, get_vac_info
139
195
  ```
140
196
 
197
+ Your FastAPI app now includes:
198
+ - All VAC endpoints
199
+ - MCP server at /mcp (for Claude Desktop/Code to connect)
200
+ - Built-in VAC tools: vac_stream, vac_query, list_available_vacs, get_vac_info
201
+
141
202
  ## Adding Custom MCP Tools
142
203
 
143
204
  ### Method 1: Using Decorators
@@ -416,6 +477,158 @@ class VACRoutesFastAPI:
416
477
 
417
478
  self.register_routes()
418
479
 
480
+ @staticmethod
481
+ def create_app_with_mcp(
482
+ title: str = "VAC Application",
483
+ stream_interpreter: Optional[callable] = None,
484
+ vac_interpreter: Optional[callable] = None,
485
+ app_lifespan: Optional[callable] = None,
486
+ **kwargs
487
+ ) -> tuple[FastAPI, 'VACRoutesFastAPI']:
488
+ """
489
+ Helper method to create a FastAPI app with proper MCP lifespan management.
490
+
491
+ This method simplifies the setup process by handling the lifespan combination
492
+ automatically, avoiding the need for the double initialization pattern.
493
+ MCP server is automatically enabled when using this method.
494
+
495
+ Args:
496
+ title: Title for the FastAPI app
497
+ stream_interpreter: Streaming interpreter function
498
+ vac_interpreter: Non-streaming interpreter function
499
+ app_lifespan: Optional app lifespan context manager
500
+ **kwargs: Additional arguments passed to VACRoutesFastAPI (except enable_mcp_server)
501
+
502
+ Returns:
503
+ Tuple of (FastAPI app, VACRoutesFastAPI instance)
504
+
505
+ Example:
506
+ ```python
507
+ from sunholo.agents.fastapi import VACRoutesFastAPI
508
+
509
+ async def my_interpreter(question, vector_name, chat_history, callback, **kwargs):
510
+ # Your logic here
511
+ return {"answer": "response", "sources": []}
512
+
513
+ # Single call to set up everything (MCP is automatically enabled)
514
+ app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(
515
+ title="My VAC App",
516
+ stream_interpreter=my_interpreter
517
+ )
518
+
519
+ # Add custom endpoints
520
+ @app.get("/custom")
521
+ async def custom_endpoint():
522
+ return {"message": "Custom endpoint"}
523
+
524
+ if __name__ == "__main__":
525
+ import uvicorn
526
+ uvicorn.run(app, host="0.0.0.0", port=8000)
527
+ ```
528
+ """
529
+ from contextlib import asynccontextmanager
530
+
531
+ # Default app lifespan if not provided
532
+ if app_lifespan is None:
533
+ @asynccontextmanager
534
+ async def app_lifespan(app: FastAPI):
535
+ yield
536
+
537
+ # Import here to avoid circular imports
538
+ if VACMCPServer:
539
+ from fastmcp import FastMCP
540
+
541
+ # Create MCP server directly to get its lifespan
542
+ mcp_server = FastMCP("sunholo-vac-fastapi-server")
543
+
544
+ # Register built-in VAC tools directly
545
+ from sunholo.mcp.vac_tools import register_vac_tools
546
+ register_vac_tools(mcp_server, None)
547
+
548
+ # Get the MCP app with path="" so when mounted at /mcp it's accessible at /mcp
549
+ mcp_app = mcp_server.http_app(path="", stateless_http=True)
550
+
551
+ # Create combined lifespan
552
+ @asynccontextmanager
553
+ async def combined_lifespan(app: FastAPI):
554
+ async with app_lifespan(app):
555
+ async with mcp_app.lifespan(app):
556
+ yield
557
+
558
+ # Create the actual app with combined lifespan
559
+ app = FastAPI(
560
+ title=title,
561
+ lifespan=combined_lifespan
562
+ )
563
+
564
+ # Mount the MCP app at /mcp
565
+ app.mount("/mcp", mcp_app)
566
+
567
+ # Now create VAC routes WITHOUT MCP (since we already mounted it)
568
+ vac_routes = VACRoutesFastAPI(
569
+ app,
570
+ stream_interpreter=stream_interpreter,
571
+ vac_interpreter=vac_interpreter,
572
+ enable_mcp_server=False, # Don't enable again since we manually mounted
573
+ **kwargs
574
+ )
575
+
576
+ # Store reference to MCP server for tool registration
577
+ vac_routes.vac_mcp_server = type('MockMCPServer', (), {
578
+ 'add_tool': lambda self, func, name=None, desc=None: mcp_server.tool(func) if name is None else mcp_server.tool(name=name)(func),
579
+ 'server': mcp_server
580
+ })()
581
+ else:
582
+ # No MCP support available
583
+ app = FastAPI(
584
+ title=title,
585
+ lifespan=app_lifespan
586
+ )
587
+
588
+ vac_routes = VACRoutesFastAPI(
589
+ app,
590
+ stream_interpreter=stream_interpreter,
591
+ vac_interpreter=vac_interpreter,
592
+ enable_mcp_server=False,
593
+ **kwargs
594
+ )
595
+
596
+ return app, vac_routes
597
+
598
+ def get_mcp_lifespan(self):
599
+ """
600
+ Get the MCP app's lifespan for manual lifespan management.
601
+
602
+ Returns:
603
+ The MCP app's lifespan if MCP server is enabled, None otherwise.
604
+
605
+ Example:
606
+ ```python
607
+ from contextlib import asynccontextmanager
608
+
609
+ # Create temp app to get MCP lifespan
610
+ temp_app = FastAPI()
611
+ vac_routes = VACRoutesFastAPI(temp_app, ..., enable_mcp_server=True)
612
+ mcp_lifespan = vac_routes.get_mcp_lifespan()
613
+
614
+ # Combine with your app's lifespan
615
+ @asynccontextmanager
616
+ async def combined_lifespan(app: FastAPI):
617
+ async with my_app_lifespan(app):
618
+ if mcp_lifespan:
619
+ async with mcp_lifespan(app):
620
+ yield
621
+ else:
622
+ yield
623
+
624
+ app = FastAPI(lifespan=combined_lifespan)
625
+ ```
626
+ """
627
+ if self.vac_mcp_server:
628
+ mcp_app = self.vac_mcp_server.get_http_app()
629
+ return mcp_app.lifespan
630
+ return None
631
+
419
632
  async def vac_interpreter_default(self, question: str, vector_name: str, chat_history=None, **kwargs):
420
633
  """Default VAC interpreter that uses the stream interpreter without streaming."""
421
634
  class NoOpCallback:
@@ -484,11 +697,35 @@ class VACRoutesFastAPI:
484
697
  if self.enable_mcp_server and self.vac_mcp_server:
485
698
  try:
486
699
  mcp_app = self.vac_mcp_server.get_http_app()
487
- self.app.mount("/mcp", mcp_app)
488
- log.info("MCP server mounted at /mcp endpoint")
700
+
701
+ # Note: FastAPI doesn't expose lifespan as a public attribute,
702
+ # so we can't easily check if it's configured. The error will be
703
+ # caught below if lifespan is missing.
704
+
705
+ # Mount at root - the MCP app already has /mcp path configured
706
+ self.app.mount("", mcp_app)
707
+ log.info("✅ MCP server mounted at /mcp endpoint")
708
+
709
+ except RuntimeError as e:
710
+ if "Task group is not initialized" in str(e):
711
+ error_msg = (
712
+ "MCP server initialization failed: Lifespan not configured properly.\n"
713
+ "The FastAPI app must be created with the MCP lifespan.\n\n"
714
+ "Quick fix: Use the helper method:\n"
715
+ " app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(\n"
716
+ " stream_interpreter=your_interpreter,\n"
717
+ " enable_mcp_server=True\n"
718
+ " )\n\n"
719
+ "Or manually configure the lifespan - see documentation for details."
720
+ )
721
+ log.error(error_msg)
722
+ raise RuntimeError(error_msg) from e
723
+ else:
724
+ log.error(f"Failed to mount MCP server: {e}")
725
+ raise RuntimeError(f"MCP server initialization failed: {e}") from e
489
726
  except Exception as e:
490
727
  log.error(f"Failed to mount MCP server: {e}")
491
- raise RuntimeError(f"MCP server initialization failed: {e}")
728
+ raise RuntimeError(f"MCP server initialization failed: {e}") from e
492
729
 
493
730
  # A2A agent endpoints
494
731
  if self.enable_a2a_agent:
sunholo/mcp/__init__.py CHANGED
@@ -26,4 +26,13 @@ except ImportError as e:
26
26
  print(f"Warning: VACMCPServer not available - {e}")
27
27
  VACMCPServer = None
28
28
 
29
- __all__ = ['MCPClientManager', 'VACMCPServer']
29
+ # SSE utilities are always available
30
+ from .sse_utils import parse_sse_response, is_sse_response, extract_sse_data
31
+
32
+ __all__ = [
33
+ 'MCPClientManager',
34
+ 'VACMCPServer',
35
+ 'parse_sse_response',
36
+ 'is_sse_response',
37
+ 'extract_sse_data'
38
+ ]
@@ -0,0 +1,105 @@
1
+ # Copyright [2024] [Holosun ApS]
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Utilities for parsing Server-Sent Events (SSE) responses from MCP servers.
17
+ """
18
+
19
+ import json
20
+ from typing import Any, Dict, Optional
21
+
22
+
23
+ def parse_sse_response(text: str) -> Dict[str, Any]:
24
+ """
25
+ Parse SSE-formatted response from MCP server.
26
+
27
+ FastMCP returns responses in SSE format when using HTTP transport,
28
+ even with stateless_http=True. This function extracts the JSON data
29
+ from the SSE format.
30
+
31
+ Args:
32
+ text: Raw response text from MCP server
33
+
34
+ Returns:
35
+ Parsed JSON data from the SSE response
36
+
37
+ Raises:
38
+ ValueError: If the response cannot be parsed
39
+
40
+ Example:
41
+ >>> response_text = 'event: message\\ndata: {"jsonrpc": "2.0", "id": 1, "result": {...}}'
42
+ >>> data = parse_sse_response(response_text)
43
+ >>> print(data['result'])
44
+ """
45
+ # Check if it's SSE format
46
+ if text.startswith('event:') or text.startswith('data:'):
47
+ # Parse SSE format - extract JSON from data: line
48
+ lines = text.split('\n')
49
+
50
+ # Find the data line
51
+ data_line = None
52
+ for line in lines:
53
+ if line.startswith('data:'):
54
+ data_line = line
55
+ break
56
+
57
+ if data_line:
58
+ # Remove 'data:' prefix and parse JSON
59
+ json_str = data_line[5:].strip()
60
+ try:
61
+ return json.loads(json_str)
62
+ except json.JSONDecodeError as e:
63
+ raise ValueError(f"Failed to parse JSON from SSE data: {e}")
64
+ else:
65
+ raise ValueError("No data line found in SSE response")
66
+ else:
67
+ # Try parsing as regular JSON
68
+ try:
69
+ return json.loads(text)
70
+ except json.JSONDecodeError as e:
71
+ raise ValueError(f"Response is not valid JSON or SSE format: {e}")
72
+
73
+
74
+ def is_sse_response(text: str) -> bool:
75
+ """
76
+ Check if a response is in SSE format.
77
+
78
+ Args:
79
+ text: Response text to check
80
+
81
+ Returns:
82
+ True if the response appears to be SSE format
83
+ """
84
+ return text.startswith('event:') or text.startswith('data:')
85
+
86
+
87
+ def extract_sse_data(text: str) -> Optional[str]:
88
+ """
89
+ Extract the data portion from an SSE response.
90
+
91
+ Args:
92
+ text: SSE-formatted response text
93
+
94
+ Returns:
95
+ The extracted data string, or None if not found
96
+ """
97
+ if not is_sse_response(text):
98
+ return None
99
+
100
+ lines = text.split('\n')
101
+ for line in lines:
102
+ if line.startswith('data:'):
103
+ return line[5:].strip()
104
+
105
+ return None
@@ -73,7 +73,8 @@ class VACMCPServer:
73
73
 
74
74
  def get_http_app(self):
75
75
  """Get the HTTP app for mounting in FastAPI."""
76
- return self.server.get_app()
76
+ # Following FastMCP docs: when mounted at root "", path="/mcp" gives us /mcp endpoint
77
+ return self.server.http_app(path="/mcp")
77
78
 
78
79
  def add_tool(self, func: Callable, name: str = None, description: str = None):
79
80
  """
sunholo/mcp/vac_tools.py CHANGED
@@ -54,12 +54,9 @@ def get_vac_config(vector_name: str = None) -> 'ConfigManager':
54
54
 
55
55
  default_vac = os.getenv("DEFAULT_VAC_NAME", "demo")
56
56
  vac_name = vector_name or default_vac
57
- vac_config_folder = os.getenv("VAC_CONFIG_FOLDER")
58
57
 
59
- if vac_config_folder:
60
- return ConfigManager(vac_name, config_folder=vac_config_folder)
61
- else:
62
- return ConfigManager(vac_name)
58
+ # ConfigManager uses VAC_CONFIG_FOLDER env var automatically
59
+ return ConfigManager(vac_name)
63
60
 
64
61
 
65
62
  async def call_vac_async(question: str, vector_name: str, chat_history: List[Dict[str, str]] = None) -> str:
@@ -241,9 +238,10 @@ def register_vac_tools(server: 'FastMCP', registry: 'MCPToolRegistry' = None):
241
238
 
242
239
  # Register tools in registry if provided
243
240
  if registry:
244
- registry.register_tool("vac_stream", vac_stream)
245
- registry.register_tool("vac_query", vac_query)
246
- registry.register_tool("list_available_vacs", list_available_vacs)
247
- registry.register_tool("get_vac_info", get_vac_info)
241
+ # Extract the underlying function from FunctionTool objects
242
+ registry.register_tool("vac_stream", vac_stream.fn if hasattr(vac_stream, 'fn') else vac_stream)
243
+ registry.register_tool("vac_query", vac_query.fn if hasattr(vac_query, 'fn') else vac_query)
244
+ registry.register_tool("list_available_vacs", list_available_vacs.fn if hasattr(list_available_vacs, 'fn') else list_available_vacs)
245
+ registry.register_tool("get_vac_info", get_vac_info.fn if hasattr(get_vac_info, 'fn') else get_vac_info)
248
246
 
249
247
  log.info("Registered built-in VAC tools with MCP server")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.144.2
3
+ Version: 0.144.4
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
@@ -16,7 +16,7 @@ sunholo/agents/swagger.py,sha256=2tzGmpveUMmTREykZvVnDj3j295wyOMu7mUFDnXdY3c,106
16
16
  sunholo/agents/fastapi/__init__.py,sha256=f7x7kiEjaNyBiOwJHLJ4vdOiePqkXdI52sIAAHtS-ms,141
17
17
  sunholo/agents/fastapi/base.py,sha256=W-cyF8ZDUH40rc-c-Apw3-_8IIi2e4Y9qRtnoVnsc1Q,2521
18
18
  sunholo/agents/fastapi/qna_routes.py,sha256=lKHkXPmwltu9EH3RMwmD153-J6pE7kWQ4BhBlV3to-s,3864
19
- sunholo/agents/fastapi/vac_routes.py,sha256=jV7Kb9CG4qdlntSHZuI_8UALjFuwYOh5ovrZpDkixzg,51909
19
+ sunholo/agents/fastapi/vac_routes.py,sha256=tDZ-2U6UAHlloFpH-5HDd2Ob_80GiOr5CMO6XZPDGXM,60877
20
20
  sunholo/agents/flask/__init__.py,sha256=dEoByI3gDNUOjpX1uVKP7uPjhfFHJubbiaAv3xLopnk,63
21
21
  sunholo/agents/flask/base.py,sha256=vnpxFEOnCmt9humqj-jYPLfJcdwzsop9NorgkJ-tSaU,1756
22
22
  sunholo/agents/flask/vac_routes.py,sha256=kaPUDyIH5KhCgeCEtag97qErGVZfqpY1ZEiX3y1_r-s,57505
@@ -115,15 +115,16 @@ sunholo/llamaindex/llamaindex_class.py,sha256=PnpPoc7LpP7xvKIXYu-UvI4ehj67pGhE1E
115
115
  sunholo/llamaindex/user_history.py,sha256=ZtkecWuF9ORduyGB8kF8gP66bm9DdvCI-ZiK6Kt-cSE,2265
116
116
  sunholo/lookup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
117
  sunholo/lookup/model_lookup.yaml,sha256=O7o-jP53MLA06C8pI-ILwERShO-xf6z_258wtpZBv6A,739
118
- sunholo/mcp/__init__.py,sha256=Bi0ZYMvWuf1AL_QSrMAREVVdTZFiIokGwrytBXKBJyc,1028
118
+ sunholo/mcp/__init__.py,sha256=hvVdBeXrYZq1saa57xlN88e7KIi4oA0VFEpgLJttTcs,1228
119
119
  sunholo/mcp/cli.py,sha256=RyTrTBQMUaNMAZ1Nyh-XKb9qGnCA5hMxpKp5-9lqfrI,821
120
120
  sunholo/mcp/cli_fastmcp.py,sha256=MWx7kJ4RHX0tTygWs247aOYr4bCKOwjnmccOPjcTVnc,6104
121
121
  sunholo/mcp/extensible_mcp_server.py,sha256=docJT800-wJLApU6kEa3lwu9FHyy1yvtJIk8JI05Z3o,8960
122
122
  sunholo/mcp/mcp_manager.py,sha256=g75vv6XvM24U7uz366slE-p76Qs4AvVcsarHSF9qIvE,5061
123
+ sunholo/mcp/sse_utils.py,sha256=LBugTxAIccQmcU2ueKIcvVlR2GjhVajwqHDnVn2s6e8,3173
123
124
  sunholo/mcp/stdio_http_bridge.py,sha256=IunHOtnjKAkRWef3SJnqnAL2r2qBRpCH2k_Q_y0Tdf8,3237
124
125
  sunholo/mcp/vac_mcp_server.py,sha256=MotoCw5lDsxCeVtwh1499yGFku9w-78xXhGkIHTUo3w,838
125
- sunholo/mcp/vac_mcp_server_fastmcp.py,sha256=ZZ0Que7XErlUQlaiMiQ8pesTttq24l7mfG7BiKk4ohs,4450
126
- sunholo/mcp/vac_tools.py,sha256=26IW5iTSEI1orbHYAlcjbBkZ1yjqQhqGqerjYkTVhko,8464
126
+ sunholo/mcp/vac_mcp_server_fastmcp.py,sha256=Xvcwl3-kqqap-yRvOrxYHmNPQi1RBAznoZKXk8b3ydQ,4556
127
+ sunholo/mcp/vac_tools.py,sha256=T7JmN4rAWYGmXbnXdpJlgE91DZGi4byzK9uWENndROA,8642
127
128
  sunholo/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
129
  sunholo/ollama/ollama_images.py,sha256=H2cpcNu88R4TwyfL_nnqkQhdvBQ2FPCAy4Ok__0yQmo,2351
129
130
  sunholo/pubsub/__init__.py,sha256=DfTEk4zmCfqn6gFxRrqDO0pOrvXTDqH-medpgYO4PGw,117
@@ -181,9 +182,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
181
182
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
182
183
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
183
184
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
184
- sunholo-0.144.2.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
185
- sunholo-0.144.2.dist-info/METADATA,sha256=QrQ-DLWAPoqQU4Jb0QVWLc7qDOJTe58siJ3AsEdGq60,18700
186
- sunholo-0.144.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
187
- sunholo-0.144.2.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
188
- sunholo-0.144.2.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
189
- sunholo-0.144.2.dist-info/RECORD,,
185
+ sunholo-0.144.4.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
186
+ sunholo-0.144.4.dist-info/METADATA,sha256=anqykTQweoYFxojD32NfblATFycMbT3x8-SYQ4uHwys,18700
187
+ sunholo-0.144.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
188
+ sunholo-0.144.4.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
189
+ sunholo-0.144.4.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
190
+ sunholo-0.144.4.dist-info/RECORD,,