vs-server 0.1.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.
vs_server/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,43 @@
1
+ from typing import Callable, List, Type
2
+
3
+
4
+ def controller(path: str, guards: List[Callable] = None):
5
+ def decorator(cls: Type) -> Type:
6
+ cls._vs_controller = {
7
+ "path": path,
8
+ "guards": guards or [],
9
+ }
10
+ return cls
11
+ return decorator
12
+
13
+
14
+ def _route(method: str, path: str = "", guards: List[Callable] = None, **kwargs):
15
+ def decorator(fn: Callable) -> Callable:
16
+ fn._vs_route = {
17
+ "method": method,
18
+ "path": path,
19
+ "guards": guards or [],
20
+ "kwargs": kwargs,
21
+ }
22
+ return fn
23
+ return decorator
24
+
25
+
26
+ def get(path: str = "", guards: List[Callable] = None, **kwargs):
27
+ return _route("GET", path, guards=guards, **kwargs)
28
+
29
+
30
+ def post(path: str = "", guards: List[Callable] = None, **kwargs):
31
+ return _route("POST", path, guards=guards, **kwargs)
32
+
33
+
34
+ def put(path: str = "", guards: List[Callable] = None, **kwargs):
35
+ return _route("PUT", path, guards=guards, **kwargs)
36
+
37
+
38
+ def delete(path: str = "", guards: List[Callable] = None, **kwargs):
39
+ return _route("DELETE", path, guards=guards, **kwargs)
40
+
41
+
42
+ def patch(path: str = "", guards: List[Callable] = None, **kwargs):
43
+ return _route("PATCH", path, guards=guards, **kwargs)
File without changes
@@ -0,0 +1,101 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+
4
+ class VsAPIException(Exception):
5
+
6
+ def __init__(self, status_code: int, error_code: str, message: str,
7
+ details: Optional[Dict[str, Any]] = None):
8
+ self.status_code = status_code
9
+ self.error_code = error_code
10
+ self.message = message
11
+ self.details = details or {}
12
+ super().__init__(message)
13
+
14
+
15
+ class NotFoundException(VsAPIException):
16
+ def __init__(self, message: str = "Resource not found", details: Optional[Dict] = None):
17
+ super().__init__(404, "NOT_FOUND", message, details)
18
+
19
+
20
+ class ValidationException(VsAPIException):
21
+ def __init__(self, message: str = "Invalid request", details: Optional[Dict] = None):
22
+ super().__init__(400, "VALIDATION_ERROR", message, details)
23
+
24
+
25
+ class UnauthorizedException(VsAPIException):
26
+ def __init__(self, message: str = "Authentication required", details: Optional[Dict] = None):
27
+ super().__init__(401, "UNAUTHORIZED", message, details)
28
+
29
+
30
+ class ForbiddenException(VsAPIException):
31
+ def __init__(self, message: str = "Access denied", details: Optional[Dict] = None):
32
+ super().__init__(403, "FORBIDDEN", message, details)
33
+
34
+
35
+ class InternalServerException(VsAPIException):
36
+ def __init__(self, message: str = "Internal server error", details: Optional[Dict] = None):
37
+ super().__init__(500, "INTERNAL_ERROR", message, details)
38
+
39
+
40
+ class ServiceUnavailableException(VsAPIException):
41
+ def __init__(self, message: str = "Service unavailable", details: Optional[Dict] = None):
42
+ super().__init__(503, "SERVICE_UNAVAILABLE", message, details)
43
+
44
+
45
+ class RateLimitException(VsAPIException):
46
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: int = 60,
47
+ details: Optional[Dict] = None):
48
+ details = details or {}
49
+ details["retry_after_seconds"] = retry_after
50
+ super().__init__(429, "RATE_LIMIT_EXCEEDED", message, details)
51
+ self.retry_after = retry_after
52
+
53
+
54
+ class TimeoutException(VsAPIException):
55
+ def __init__(self, message: str = "Request timed out", details: Optional[Dict] = None):
56
+ super().__init__(504, "TIMEOUT_ERROR", message, details)
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # FastAPI handlers — only imported inside VsFastApiServer._build_app()
61
+ # ---------------------------------------------------------------------------
62
+
63
+ def _error_response(status_code: int, code: str, message: str,
64
+ details: Dict, headers: Dict = None):
65
+ from fastapi.responses import JSONResponse
66
+ return JSONResponse(
67
+ status_code=status_code,
68
+ content={"success": False, "error": {"code": code, "message": message, "details": details}},
69
+ headers=headers or {},
70
+ )
71
+
72
+
73
+ async def vs_exception_handler(request, exc: VsAPIException):
74
+ headers = {}
75
+ if isinstance(exc, RateLimitException):
76
+ headers["Retry-After"] = str(exc.retry_after)
77
+ return _error_response(exc.status_code, exc.error_code, exc.message, exc.details, headers)
78
+
79
+
80
+ async def validation_exception_handler(request, exc):
81
+ from fastapi.exceptions import RequestValidationError
82
+ if isinstance(exc, RequestValidationError):
83
+ errors = [
84
+ {"field": ".".join(str(x) for x in e.get("loc", [])),
85
+ "message": e.get("msg", ""), "type": e.get("type", "")}
86
+ for e in exc.errors()
87
+ ]
88
+ return _error_response(400, "VALIDATION_ERROR", "Invalid request data", {"errors": errors})
89
+ return _error_response(500, "INTERNAL_ERROR", "An unexpected error occurred", {})
90
+
91
+
92
+ async def permission_error_handler(request, exc: PermissionError):
93
+ return _error_response(403, "FORBIDDEN", str(exc), {})
94
+
95
+
96
+ async def value_error_handler(request, exc: ValueError):
97
+ return _error_response(400, "BAD_REQUEST", str(exc), {})
98
+
99
+
100
+ async def unhandled_exception_handler(request, exc: Exception):
101
+ return _error_response(500, "INTERNAL_ERROR", "An unexpected error occurred", {})
@@ -0,0 +1,13 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class VsServerHealth(BaseModel):
8
+ status: str = Field(..., description="healthy | degraded | unhealthy")
9
+ name: str
10
+ version: str
11
+ uptime_seconds: Optional[float] = None
12
+ cache: Optional[str] = Field(None, description="healthy | unhealthy")
13
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
File without changes
@@ -0,0 +1,177 @@
1
+ import time
2
+ from contextlib import asynccontextmanager
3
+ from typing import Optional
4
+
5
+ from vs_common.config.vs_base_config import VsBaseConfig
6
+ from vs_server.server.vs_server import VsServer
7
+ from vs_server.schema.exceptions import ( # noqa — deferred for optional fastapi dep
8
+ VsAPIException, vs_exception_handler, validation_exception_handler,
9
+ permission_error_handler, value_error_handler, unhandled_exception_handler,
10
+ )
11
+
12
+ class VsFastApiServer(VsServer):
13
+
14
+ def __init__(self, config: VsBaseConfig):
15
+ try:
16
+ import fastapi # noqa
17
+ except ImportError:
18
+ raise ImportError("FastAPI is not installed. Run: pip install vs-server[fastapi]")
19
+ super().__init__(config)
20
+ self._startup_time: Optional[float] = None
21
+ self._app = self._build_app()
22
+
23
+ # ------------------------------------------------------------------
24
+ # VsServer contract
25
+ # ------------------------------------------------------------------
26
+
27
+ def add_router(self, router, prefix: str = "") -> None:
28
+ self._app.include_router(router, prefix=prefix)
29
+
30
+ def add_controller(self, instance, guards: list = None) -> None:
31
+ import inspect
32
+ from fastapi import APIRouter, Depends
33
+
34
+ cls = type(instance)
35
+ if not hasattr(cls, '_vs_controller'):
36
+ raise ValueError(f"{cls.__name__} must be decorated with @controller")
37
+
38
+ meta = cls._vs_controller
39
+ tag = cls.__name__.replace("Controller", "")
40
+ all_guards = (guards or []) + meta["guards"]
41
+ dependencies = [Depends(g) for g in all_guards]
42
+ router = APIRouter(prefix=meta["path"], tags=[tag], dependencies=dependencies)
43
+
44
+ for _, method in inspect.getmembers(instance, predicate=inspect.ismethod):
45
+ if hasattr(method, '_vs_route'):
46
+ route_meta = method._vs_route
47
+ merged_guards = all_guards + route_meta["guards"]
48
+ merged_deps = [Depends(g) for g in merged_guards]
49
+ getattr(router, route_meta["method"].lower())(
50
+ route_meta["path"],
51
+ dependencies=merged_deps,
52
+ **route_meta["kwargs"]
53
+ )(method)
54
+
55
+ self._app.include_router(router)
56
+
57
+ def get_app(self):
58
+ return self._app
59
+
60
+ def run(self) -> None:
61
+ import uvicorn
62
+ cfg = self._config
63
+ host = cfg.get("server.host", "127.0.0.1")
64
+ port = cfg.get("server.port", 8000, int)
65
+ reload = cfg.get("server.reload", False, bool)
66
+ workers = cfg.get("server.workers", 1, int)
67
+ log_level = cfg.get("server.log_level", "info").lower()
68
+
69
+ self._print_banner(host, port, reload)
70
+
71
+ uvicorn_config = {
72
+ "app": self._app,
73
+ "host": host,
74
+ "port": port,
75
+ "log_level": log_level,
76
+ }
77
+ if reload:
78
+ uvicorn_config["reload"] = True
79
+ elif workers > 1:
80
+ uvicorn_config["workers"] = workers
81
+
82
+ uvicorn.run(**uvicorn_config)
83
+
84
+ # ------------------------------------------------------------------
85
+ # Internal
86
+ # ------------------------------------------------------------------
87
+
88
+ def _build_app(self):
89
+ from fastapi import FastAPI
90
+
91
+
92
+ @asynccontextmanager
93
+ async def lifespan(app: FastAPI):
94
+ self._startup_time = time.time()
95
+ await self.startup()
96
+ yield
97
+ await self.shutdown()
98
+
99
+ app = FastAPI(
100
+ title=self.name,
101
+ description=self.description,
102
+ version=self.version,
103
+ lifespan=lifespan,
104
+ )
105
+
106
+ self._register_exception_handlers(app)
107
+ self._register_middleware(app)
108
+
109
+ app.include_router(self._build_core_router(), prefix=self.base_url)
110
+ return app
111
+
112
+ def _register_exception_handlers(self, app) -> None:
113
+ from fastapi.exceptions import RequestValidationError
114
+ from vs_server.schema.exceptions import (
115
+ VsAPIException, vs_exception_handler, validation_exception_handler,
116
+ permission_error_handler, value_error_handler, unhandled_exception_handler,
117
+ )
118
+ app.add_exception_handler(VsAPIException, vs_exception_handler)
119
+ app.add_exception_handler(RequestValidationError, validation_exception_handler)
120
+ app.add_exception_handler(PermissionError, permission_error_handler)
121
+ app.add_exception_handler(ValueError, value_error_handler)
122
+ app.add_exception_handler(Exception, unhandled_exception_handler)
123
+
124
+ def _register_middleware(self, app) -> None:
125
+ from starlette.middleware.cors import CORSMiddleware
126
+ cors_origins = self._config.get("api.cors_origins", "*")
127
+ if isinstance(cors_origins, str):
128
+ cors_origins = [o.strip() for o in cors_origins.split(",")]
129
+ app.add_middleware(
130
+ CORSMiddleware,
131
+ allow_origins=cors_origins,
132
+ allow_credentials=True,
133
+ allow_methods=["*"],
134
+ allow_headers=["*"],
135
+ )
136
+
137
+ def _build_core_router(self):
138
+ from fastapi import APIRouter
139
+ from starlette import status
140
+ from vs_common.cache.vs_cache_manager import VsCacheManager
141
+ from vs_server.schema.vs_server_health import VsServerHealth
142
+
143
+ router = APIRouter()
144
+
145
+ @router.get("/", status_code=status.HTTP_200_OK)
146
+ async def root():
147
+ return {"service": self.name, "version": self.version, "status": "running"}
148
+
149
+ @router.get("/health", response_model=VsServerHealth, status_code=status.HTTP_200_OK)
150
+ async def health():
151
+ uptime = time.time() - self._startup_time if self._startup_time else None
152
+ cache_status = "healthy" if await VsCacheManager.ping() else "unhealthy"
153
+ overall = "healthy" if cache_status == "healthy" else "degraded"
154
+ return VsServerHealth(
155
+ status=overall,
156
+ name=self.name,
157
+ version=self.version,
158
+ uptime_seconds=uptime,
159
+ cache=cache_status,
160
+ )
161
+
162
+ return router
163
+
164
+ def _print_banner(self, host: str, port: int, reload: bool) -> None:
165
+ mode = "Development (reload)" if reload else "Production"
166
+ self._logger.info("=" * 50)
167
+ self._logger.info(f" {self.name} v{self.version}")
168
+ self._logger.info("=" * 50)
169
+ self._logger.info(f" Host : {host}")
170
+ self._logger.info(f" Port : {port}")
171
+ self._logger.info(f" Mode : {mode}")
172
+ self._logger.info(f" Docs : http://{host}:{port}/docs")
173
+ if self.base_url:
174
+ self._logger.info(f" Health : http://{host}:{port}{self.base_url}/health")
175
+ else:
176
+ self._logger.info(f" Health : http://{host}:{port}/health")
177
+ self._logger.info("=" * 50)
@@ -0,0 +1,45 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from vs_common.config.vs_base_config import VsBaseConfig
4
+ from vs_common.log.vs_log_manager import VsLogManager
5
+
6
+
7
+ class VsServer(ABC):
8
+
9
+ def __init__(self, config: VsBaseConfig):
10
+ self._config = config
11
+ self.name = config.get("server.name", "vs-server")
12
+ self.version = config.get("server.version", "0.1.0")
13
+ self.description = config.get("server.description", "App server developed by Viveka Sutra")
14
+ self.base_url = config.get("server.base_url", "")
15
+ self._logger = VsLogManager.get_instance(self.__class__.__name__)
16
+
17
+ # ------------------------------------------------------------------
18
+ # Template methods — override for custom lifecycle logic
19
+ # ------------------------------------------------------------------
20
+
21
+ async def startup(self) -> None:
22
+ self._logger.info(f"{self.name} v{self.version} starting up")
23
+
24
+ async def shutdown(self) -> None:
25
+ self._logger.info(f"{self.name} stopped")
26
+
27
+ # ------------------------------------------------------------------
28
+ # Abstract — each framework implementation provides these
29
+ # ------------------------------------------------------------------
30
+
31
+ @abstractmethod
32
+ def add_router(self, router, prefix: str = "") -> None:
33
+ """Register a router with the underlying framework."""
34
+
35
+ @abstractmethod
36
+ def get_app(self):
37
+ """Return the underlying framework application instance."""
38
+
39
+ @abstractmethod
40
+ def run(self) -> None:
41
+ """Start the server. All config comes from config.ini."""
42
+
43
+ @abstractmethod
44
+ def add_controller(self, instance, guards: list = None) -> None:
45
+ """Add Controllers"""
@@ -0,0 +1,735 @@
1
+ Metadata-Version: 2.4
2
+ Name: vs-server
3
+ Version: 0.1.0
4
+ Summary: HTTP server abstraction for Viveka Sutra — FastAPI wrapper with lifecycle, routing, and middleware
5
+ License: Apache License
6
+ Version 2.0, January 2004
7
+ http://www.apache.org/licenses/
8
+
9
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
10
+
11
+ 1. Definitions.
12
+
13
+ "License" shall mean the terms and conditions for use, reproduction,
14
+ and distribution as defined by Sections 1 through 9 of this document.
15
+
16
+ "Licensor" shall mean the copyright owner or entity authorized by
17
+ the copyright owner that is granting the License.
18
+
19
+ "Legal Entity" shall mean the union of the acting entity and all
20
+ other entities that control, are controlled by, or are under common
21
+ control with that entity. For the purposes of this definition,
22
+ "control" means (i) the power, direct or indirect, to cause the
23
+ direction or management of such entity, whether by contract or
24
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
25
+ outstanding shares, or (iii) beneficial ownership of such entity.
26
+
27
+ "You" (or "Your") shall mean an individual or Legal Entity
28
+ exercising permissions granted by this License.
29
+
30
+ "Source" form shall mean the preferred form for making modifications,
31
+ including but not limited to software source code, documentation
32
+ source, and configuration files.
33
+
34
+ "Object" form shall mean any form resulting from mechanical
35
+ transformation or translation of a Source form, including but
36
+ not limited to compiled object code, generated documentation,
37
+ and conversions to other media types.
38
+
39
+ "Work" shall mean the work of authorship made available under
40
+ the License, as indicated by a copyright notice that is included in
41
+ or attached to the work (an example is provided in the Appendix below).
42
+
43
+ "Derivative Works" shall mean any work, whether in Source or Object
44
+ form, that is based on (or derived from) the Work and for which the
45
+ editorial revisions, annotations, elaborations, or other modifications
46
+ represent, as a whole, an original work of authorship. For the purposes
47
+ of this License, Derivative Works shall not include works that remain
48
+ separable from, or merely link (or bind by name) to the interfaces of,
49
+ the Work and derivative works thereof.
50
+
51
+ "Contribution" shall mean, as submitted to the Licensor for inclusion
52
+ in the Work by the copyright owner or by an individual or Legal Entity
53
+ authorized to submit on behalf of the copyright owner. For the purposes
54
+ of this definition, "submitted" means any form of electronic, verbal,
55
+ or written communication sent to the Licensor or its representatives,
56
+ including but not limited to communication on electronic mailing lists,
57
+ source code control systems, and issue tracking systems that are managed
58
+ by, or on behalf of, the Licensor for the purpose of discussing and
59
+ improving the Work, but excluding communication that is conspicuously
60
+ marked or designated in writing by the copyright owner as "Not a
61
+ Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any Legal Entity on behalf of
64
+ whom a Contribution has been received by the Licensor and incorporated
65
+ within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by the combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a cross-claim
84
+ or counterclaim in a lawsuit) alleging that the Work or any
85
+ Contribution embodied within the Work constitutes direct or
86
+ contributory patent infringement, then any patent licenses granted to
87
+ You under this License for that Work shall terminate as of the date
88
+ such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or Derivative
96
+ Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" file as part of its distribution,
108
+ You must include a readable copy of the attribution notices
109
+ contained within such NOTICE file, in at least one of the
110
+ following places: within a NOTICE text file distributed as part
111
+ of the Derivative Works; within the Source form or documentation,
112
+ if provided along with the Derivative Works; or, within a display
113
+ generated by the Derivative Works, if and wherever such
114
+ third-party notices normally appear. The contents of the NOTICE
115
+ file are for informational purposes only and do not modify the
116
+ License. You may add Your own attribution notices within
117
+ Derivative Works that You distribute, alongside or as an addendum
118
+ to the NOTICE text from the Work, provided that such additional
119
+ attribution notices cannot be construed as modifying the License.
120
+
121
+ You may add Your own license statement for Your modifications and
122
+ may provide additional grant of rights to use, modify, and distribute
123
+ Your modifications.
124
+
125
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
126
+ any Contribution intentionally submitted for inclusion in the Work
127
+ by You to the Licensor shall be under the terms and conditions of
128
+ this License, without any additional terms or conditions.
129
+ Notwithstanding the above, nothing herein shall supersede or modify
130
+ the terms of any separate license agreement you may have executed
131
+ with Licensor regarding such Contributions.
132
+
133
+ 6. Trademarks. This License does not grant permission to use the trade
134
+ names, trademarks, service marks, or product names of the Licensor,
135
+ except as required for reasonable and customary use in describing the
136
+ origin of the Work and reproducing the content of the NOTICE file.
137
+
138
+ 7. Disclaimer of Warranty. Unless required by applicable law or
139
+ agreed to in writing, Licensor provides the Work (and each
140
+ Contributor provides its Contributions) on an "AS IS" BASIS,
141
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
142
+ implied, including, without limitation, any conditions of TITLE,
143
+ NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
144
+ PURPOSE. You are solely responsible for determining the
145
+ appropriateness of using or reproducing the Work and assume any
146
+ risks associated with Your exercise of permissions under this License.
147
+
148
+ 8. Limitation of Liability. In no event and under no legal theory,
149
+ whether in tort (including negligence), contract, or otherwise,
150
+ unless required by applicable law (such as deliberate and grossly
151
+ negligent acts) or agreed to in writing, shall any Contributor be
152
+ liable to You for damages, including any direct, indirect, special,
153
+ incidental, or exemplary damages of any character arising as a
154
+ result of this License or out of the use or inability to use the
155
+ Work (including but not limited to damages for loss of goodwill,
156
+ work stoppage, computer failure or malfunction, or all other
157
+ commercial damages or losses), even if such Contributor has been
158
+ advised of the possibility of such damages.
159
+
160
+ 9. Accepting Warranty or Additional Liability. While redistributing
161
+ the Work or Derivative Works thereof, You may choose to offer,
162
+ and charge a fee for, acceptance of support, warranty, indemnity,
163
+ or other liability obligations and/or rights consistent with this
164
+ License. However, in accepting such obligations, You may act only
165
+ on Your own behalf and on Your sole responsibility, not on behalf
166
+ of any other Contributor, and only if You agree to indemnify,
167
+ defend, and hold each Contributor harmless for any liability
168
+ incurred by, or claims asserted against, such Contributor by reason
169
+ of your accepting any such warranty or additional liability.
170
+
171
+ END OF TERMS AND CONDITIONS
172
+
173
+ Copyright 2026 VivekaSutra. All rights reserved.
174
+
175
+ Licensed under the Apache License, Version 2.0 (the "License");
176
+ you may not use this file except in compliance with the License.
177
+ You may obtain a copy of the License at
178
+
179
+ http://www.apache.org/licenses/LICENSE-2.0
180
+
181
+ Unless required by applicable law or agreed to in writing, software
182
+ distributed under the License is distributed on an "AS IS" BASIS,
183
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
184
+ implied. See the License for the specific language governing
185
+ permissions and limitations under the License.
186
+
187
+ Project-URL: Homepage, https://vivekasutra.com/
188
+ Project-URL: Source, https://github.com/vivekasutra/viveka-mula
189
+ Keywords: fastapi,server,http,api,viveka,vs
190
+ Classifier: Development Status :: 3 - Alpha
191
+ Classifier: Intended Audience :: Developers
192
+ Classifier: License :: Other/Proprietary License
193
+ Classifier: Programming Language :: Python :: 3.11
194
+ Classifier: Programming Language :: Python :: 3.12
195
+ Classifier: Programming Language :: Python :: 3.13
196
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
197
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
198
+ Classifier: Framework :: AsyncIO
199
+ Classifier: Typing :: Typed
200
+ Requires-Python: >=3.11
201
+ Description-Content-Type: text/markdown
202
+ License-File: LICENSE.txt
203
+ Requires-Dist: vs-common>=0.1.0
204
+ Requires-Dist: pydantic>=2.0
205
+ Provides-Extra: fastapi
206
+ Requires-Dist: fastapi>=0.100; extra == "fastapi"
207
+ Requires-Dist: uvicorn>=0.23; extra == "fastapi"
208
+ Provides-Extra: flask
209
+ Requires-Dist: flask>=3.0; extra == "flask"
210
+ Provides-Extra: dev
211
+ Requires-Dist: build; extra == "dev"
212
+ Requires-Dist: twine; extra == "dev"
213
+ Requires-Dist: pytest>=8.0; extra == "dev"
214
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
215
+ Requires-Dist: fastapi>=0.100; extra == "dev"
216
+ Requires-Dist: uvicorn>=0.23; extra == "dev"
217
+ Requires-Dist: httpx>=0.27; extra == "dev"
218
+ Dynamic: license-file
219
+
220
+ # vs-server
221
+
222
+ HTTP server library for Python — built on FastAPI. Designed to feel like Spring MVC: annotate a controller class, define routes with method decorators, add guards for auth, and start the server with one line.
223
+
224
+ ---
225
+
226
+ ## The Problem It Solves
227
+
228
+ Without `vs-server`, every FastAPI service repeats the same boilerplate:
229
+
230
+ ```python
231
+ # Without vs-server — repeated in every service
232
+ app = FastAPI(title="my-service", lifespan=lifespan)
233
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
234
+ app.add_exception_handler(RequestValidationError, handler)
235
+ app.add_exception_handler(Exception, handler)
236
+
237
+ router = APIRouter(prefix="/users", tags=["Users"])
238
+
239
+ @router.get("", response_model=list[UserResponse])
240
+ async def list_users(): ...
241
+
242
+ @router.post("", status_code=201, response_model=UserResponse)
243
+ async def create_user(body: UserRequest): ...
244
+
245
+ app.include_router(router)
246
+ uvicorn.run(app, host="0.0.0.0", port=8080)
247
+ ```
248
+
249
+ With `vs-server`:
250
+
251
+ ```python
252
+ # With vs-server — zero boilerplate
253
+ @controller("/users")
254
+ class UserController:
255
+
256
+ @get("", response_model=list[UserResponse])
257
+ async def list_users(self): ...
258
+
259
+ @post("", status_code=201, response_model=UserResponse)
260
+ async def create_user(self, body: UserRequest): ...
261
+
262
+ VsFastApiServer(config).add_controller(UserController()).run()
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Installation
268
+
269
+ ```bash
270
+ pip install vs-server[fastapi]
271
+ ```
272
+
273
+ For future framework support:
274
+
275
+ ```bash
276
+ pip install vs-server[flask] # when available
277
+ ```
278
+
279
+ ---
280
+
281
+ ## How It All Fits Together
282
+
283
+ ```
284
+ Application Startup
285
+ └── VsLogManager.init(...) # initialize logging
286
+ └── VsDbSessionFactory(config) # optional — if using vs-db
287
+ └── VsCacheManager.init(...) # optional — if using cache
288
+
289
+ Server
290
+ └── VsFastApiServer(config)
291
+ ├── reads name/version/host/port from config.ini
292
+ ├── wires CORS, exception handlers, lifespan
293
+ ├── auto-registers VsDbMiddleware if vs-db is initialized
294
+ └── exposes / and /health
295
+
296
+ Controllers
297
+ └── @controller("/users", guards=[require_auth])
298
+ └── @get, @post, @put, @delete, @patch on methods
299
+ └── guards stack: controller guards run first, method guards append
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Application Startup
305
+
306
+ ```python
307
+ from vs_common.config.vs_ini_config import VsIniConfig
308
+ from vs_common.log.vs_log_manager import VsLogManager
309
+ from vs_common.schema.vs_log_config import VsLogConfig
310
+ from vs_server.server.vs_fast_api_server import VsFastApiServer
311
+
312
+ if __name__ == "__main__":
313
+ config = VsIniConfig("config.ini")
314
+
315
+ VsLogManager.init(VsLogConfig(
316
+ level=config.get("logging.level", "INFO"),
317
+ file_path=config.get("logging.file_path", "logs/app.log"),
318
+ ))
319
+
320
+ server = VsFastApiServer(config)
321
+ server.add_controller(UserController())
322
+ server.add_controller(OrderController())
323
+ server.run()
324
+ ```
325
+
326
+ `config.ini`:
327
+
328
+ ```ini
329
+ [server]
330
+ name = my-service
331
+ version = 1.0.0
332
+ description = My API
333
+ base_url =
334
+ host = 0.0.0.0
335
+ port = 8080
336
+ reload = false
337
+ workers = 1
338
+ log_level = info
339
+
340
+ [api]
341
+ cors_origins = https://myapp.com, https://admin.myapp.com
342
+
343
+ [logging]
344
+ level = INFO
345
+ console_enabled = true
346
+ file_enabled = true
347
+ file_path = logs/app.log
348
+ ```
349
+
350
+ ---
351
+
352
+ ## `@controller`
353
+
354
+ Marks a class as a route controller. All routes defined inside are registered under the given base path.
355
+
356
+ ```python
357
+ from vs_server.decorator.vs_controller_decorator import controller, get, post, put, delete, patch
358
+
359
+ @controller("/users")
360
+ class UserController:
361
+ ...
362
+ ```
363
+
364
+ Register with the server:
365
+
366
+ ```python
367
+ server.add_controller(UserController())
368
+ ```
369
+
370
+ The tag in Swagger/OpenAPI is auto-derived from the class name — `UserController` → `"User"`.
371
+
372
+ ---
373
+
374
+ ## HTTP Method Decorators
375
+
376
+ Define routes directly on controller methods. All FastAPI route parameters are supported via `**kwargs`.
377
+
378
+ ```python
379
+ @controller("/users")
380
+ class UserController:
381
+
382
+ @get("", response_model=list[UserResponse])
383
+ async def list_users(self): ...
384
+
385
+ @get("/{id}", response_model=UserResponse)
386
+ async def get_user(self, id: int): ...
387
+
388
+ @post("", status_code=201, response_model=UserResponse)
389
+ async def create_user(self, body: UserRequest): ...
390
+
391
+ @put("/{id}", response_model=UserResponse)
392
+ async def update_user(self, id: int, body: UserRequest): ...
393
+
394
+ @patch("/{id}", response_model=UserResponse)
395
+ async def patch_user(self, id: int, body: UserPatchRequest): ...
396
+
397
+ @delete("/{id}", status_code=204)
398
+ async def delete_user(self, id: int): ...
399
+ ```
400
+
401
+ Supported kwargs passed through to FastAPI:
402
+
403
+ | Parameter | Example | Description |
404
+ |---|---|---|
405
+ | `response_model` | `response_model=UserResponse` | Pydantic model for response serialization |
406
+ | `status_code` | `status_code=201` | HTTP status code for success |
407
+ | `dependencies` | `dependencies=[Depends(fn)]` | FastAPI dependencies |
408
+ | `tags` | `tags=["custom"]` | Override auto-generated Swagger tag |
409
+ | `summary` | `summary="List all users"` | Swagger summary |
410
+ | `deprecated` | `deprecated=True` | Mark route as deprecated in Swagger |
411
+
412
+ ---
413
+
414
+ ## Guards
415
+
416
+ Guards are callables that run before the route handler. They are the equivalent of Spring's `@PreAuthorize` or a scoped servlet filter.
417
+
418
+ ### Controller-level guards
419
+
420
+ Apply to every route in the controller:
421
+
422
+ ```python
423
+ from fastapi import Header
424
+ from vs_server.schema.exceptions import ForbiddenException
425
+
426
+ async def require_auth(authorization: str = Header(...)):
427
+ if not is_valid_token(authorization):
428
+ raise ForbiddenException("Invalid token")
429
+
430
+ @controller("/orders", guards=[require_auth])
431
+ class OrderController:
432
+
433
+ @get("")
434
+ async def list_orders(self): ... # require_auth runs
435
+
436
+ @post("")
437
+ async def create_order(self): ... # require_auth runs
438
+ ```
439
+
440
+ ### Method-level guards
441
+
442
+ Method guards **append** on top of controller guards — they never replace them:
443
+
444
+ ```python
445
+ async def require_admin(authorization: str = Header(...)):
446
+ if not is_admin(authorization):
447
+ raise ForbiddenException("Admin access required")
448
+
449
+ @controller("/users", guards=[require_auth])
450
+ class UserController:
451
+
452
+ @get("")
453
+ async def list_users(self): ... # runs: require_auth
454
+
455
+ @delete("/{id}", guards=[require_admin])
456
+ async def delete_user(self, id: int): ... # runs: require_auth → require_admin
457
+ ```
458
+
459
+ Guards are any callable — auth, rate limiting, tenant validation, feature flags:
460
+
461
+ ```python
462
+ async def rate_limit(request: Request): ...
463
+ async def validate_tenant(x_tenant_id: str = Header(...)): ...
464
+ async def require_feature(x_feature: str = Header(...)): ...
465
+
466
+ @controller("/api", guards=[require_auth, rate_limit, validate_tenant])
467
+ class ApiController: ...
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Exception Handling
473
+
474
+ ### Built-in exceptions
475
+
476
+ All exceptions produce a consistent response shape:
477
+
478
+ ```json
479
+ {
480
+ "success": false,
481
+ "error": {
482
+ "code": "NOT_FOUND",
483
+ "message": "User not found",
484
+ "details": {}
485
+ }
486
+ }
487
+ ```
488
+
489
+ | Exception | Status | Code |
490
+ |---|---|---|
491
+ | `NotFoundException` | 404 | `NOT_FOUND` |
492
+ | `ValidationException` | 400 | `VALIDATION_ERROR` |
493
+ | `UnauthorizedException` | 401 | `UNAUTHORIZED` |
494
+ | `ForbiddenException` | 403 | `FORBIDDEN` |
495
+ | `RateLimitException` | 429 | `RATE_LIMIT_EXCEEDED` |
496
+ | `TimeoutException` | 504 | `TIMEOUT_ERROR` |
497
+ | `ServiceUnavailableException` | 503 | `SERVICE_UNAVAILABLE` |
498
+ | `InternalServerException` | 500 | `INTERNAL_ERROR` |
499
+
500
+ Usage in a controller:
501
+
502
+ ```python
503
+ from vs_server.schema.exceptions import NotFoundException, ForbiddenException
504
+
505
+ @get("/{id}", response_model=UserResponse)
506
+ async def get_user(self, id: int):
507
+ user = await self.service.get_by_id(id)
508
+ if not user:
509
+ raise NotFoundException(f"User {id} not found")
510
+ return user
511
+ ```
512
+
513
+ `RateLimitException` automatically adds a `Retry-After` header to the response.
514
+
515
+ ### Custom exception handlers
516
+
517
+ Override `_register_exception_handlers()` to add your own:
518
+
519
+ ```python
520
+ class MyServer(VsFastApiServer):
521
+
522
+ def _register_exception_handlers(self, app) -> None:
523
+ super()._register_exception_handlers(app) # keep defaults
524
+ app.add_exception_handler(PaymentException, payment_error_handler)
525
+ app.add_exception_handler(InventoryException, inventory_error_handler)
526
+ ```
527
+
528
+ ---
529
+
530
+ ## Built-in Routes
531
+
532
+ Registered automatically on every server:
533
+
534
+ ### `GET /`
535
+
536
+ ```json
537
+ {
538
+ "service": "my-service",
539
+ "version": "1.0.0",
540
+ "status": "running"
541
+ }
542
+ ```
543
+
544
+ ### `GET /health`
545
+
546
+ ```json
547
+ {
548
+ "status": "healthy",
549
+ "name": "my-service",
550
+ "version": "1.0.0",
551
+ "uptime_seconds": 142.3,
552
+ "cache": "healthy",
553
+ "timestamp": "2026-07-19T10:00:00"
554
+ }
555
+ ```
556
+
557
+ `status` is `"healthy"` when all components are up, `"degraded"` when cache is unreachable.
558
+
559
+ ---
560
+
561
+ ## Lifecycle Hooks
562
+
563
+ Override `startup()` and `shutdown()` for custom logic:
564
+
565
+ ```python
566
+ class MyServer(VsFastApiServer):
567
+
568
+ async def startup(self) -> None:
569
+ await super().startup()
570
+ await self.warm_up_cache()
571
+ self._logger.info("Cache warmed up")
572
+
573
+ async def shutdown(self) -> None:
574
+ await self.flush_pending_jobs()
575
+ await super().shutdown()
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Custom Middleware
581
+
582
+ Override `_register_middleware()` to add your own middleware:
583
+
584
+ ```python
585
+ class MyServer(VsFastApiServer):
586
+
587
+ def _register_middleware(self, app) -> None:
588
+ super()._register_middleware(app) # keep CORS
589
+ app.add_middleware(MyRateLimitMiddleware)
590
+ app.add_middleware(RequestIdMiddleware)
591
+ ```
592
+
593
+ ---
594
+
595
+ ## Using with vs-db
596
+
597
+ Initialize `VsDbSessionFactory` before creating the server — `VsDbMiddleware` is registered automatically:
598
+
599
+ ```python
600
+ from vs_common.config.vs_ini_config import VsIniConfig
601
+ from vs_common.log.vs_log_manager import VsLogManager
602
+ from vs_common.schema.vs_log_config import VsLogConfig
603
+ from vs_db.session.vs_db_session_factory import VsDbSessionFactory
604
+ from vs_server.server.vs_fast_api_server import VsFastApiServer
605
+
606
+ if __name__ == "__main__":
607
+ config = VsIniConfig("config.ini")
608
+ VsLogManager.init(VsLogConfig(...))
609
+ VsDbSessionFactory(config) # vs-db initialized → middleware auto-registered
610
+
611
+ server = VsFastApiServer(config)
612
+ server.add_controller(UserController())
613
+ server.run()
614
+ ```
615
+
616
+ No manual middleware registration needed.
617
+
618
+ ---
619
+
620
+ ## Implementing Your Own Server
621
+
622
+ Extend `VsServer` to support any framework:
623
+
624
+ ```python
625
+ from vs_server.server.vs_server import VsServer
626
+ from vs_common.config.vs_base_config import VsBaseConfig
627
+
628
+ class VsFlaskServer(VsServer):
629
+
630
+ def __init__(self, config: VsBaseConfig):
631
+ try:
632
+ from flask import Flask
633
+ except ImportError:
634
+ raise ImportError("Run: pip install vs-server[flask]")
635
+ super().__init__(config)
636
+ self._app = Flask(self.name)
637
+
638
+ def add_router(self, router, prefix: str = "") -> None:
639
+ self._app.register_blueprint(router, url_prefix=prefix)
640
+
641
+ def add_controller(self, instance) -> None:
642
+ # scan for _vs_route metadata and register with Flask
643
+ ...
644
+
645
+ def get_app(self):
646
+ return self._app
647
+
648
+ def run(self) -> None:
649
+ self._app.run(
650
+ host=self._config.get("server.host", "127.0.0.1"),
651
+ port=self._config.get("server.port", 8000, int),
652
+ )
653
+ ```
654
+
655
+ ---
656
+
657
+ ## Full Example
658
+
659
+ ```python
660
+ from pydantic import BaseModel
661
+ from fastapi import Header
662
+ from vs_server.decorator.vs_controller_decorator import controller, get, post, put, delete
663
+ from vs_server.schema.exceptions import NotFoundException, ForbiddenException
664
+
665
+
666
+ class UserRequest(BaseModel):
667
+ name: str
668
+ email: str
669
+
670
+ class UserResponse(BaseModel):
671
+ id: int
672
+ name: str
673
+ email: str
674
+
675
+
676
+ async def require_auth(authorization: str = Header(...)):
677
+ if authorization != "Bearer secret":
678
+ raise ForbiddenException("Invalid token")
679
+
680
+ async def require_admin(authorization: str = Header(...)):
681
+ if authorization != "Bearer admin":
682
+ raise ForbiddenException("Admin access required")
683
+
684
+
685
+ @controller("/users", guards=[require_auth])
686
+ class UserController:
687
+
688
+ def __init__(self):
689
+ self.service = UserService()
690
+
691
+ @get("", response_model=list[UserResponse])
692
+ async def list_users(self):
693
+ return await self.service.get_all()
694
+
695
+ @get("/{id}", response_model=UserResponse)
696
+ async def get_user(self, id: int):
697
+ user = await self.service.get_by_id(id)
698
+ if not user:
699
+ raise NotFoundException(f"User {id} not found")
700
+ return user
701
+
702
+ @post("", status_code=201, response_model=UserResponse)
703
+ async def create_user(self, body: UserRequest):
704
+ return await self.service.create(body)
705
+
706
+ @put("/{id}", response_model=UserResponse)
707
+ async def update_user(self, id: int, body: UserRequest):
708
+ return await self.service.update(id, body)
709
+
710
+ @delete("/{id}", status_code=204, guards=[require_admin])
711
+ async def delete_user(self, id: int):
712
+ await self.service.delete(id)
713
+
714
+
715
+ if __name__ == "__main__":
716
+ from vs_common.config.vs_ini_config import VsIniConfig
717
+ from vs_common.log.vs_log_manager import VsLogManager
718
+ from vs_common.schema.vs_log_config import VsLogConfig
719
+ from vs_server.server.vs_fast_api_server import VsFastApiServer
720
+
721
+ config = VsIniConfig("config.ini")
722
+ VsLogManager.init(VsLogConfig(level="INFO"))
723
+
724
+ server = VsFastApiServer(config)
725
+ server.add_controller(UserController())
726
+ server.run()
727
+ ```
728
+
729
+ **What vs-server eliminated:**
730
+ - No `FastAPI()` setup
731
+ - No `add_middleware()` calls for CORS and exception handlers
732
+ - No `APIRouter()` boilerplate
733
+ - No `include_router()` calls
734
+ - No `uvicorn.run()` wiring
735
+ - Guards expressed as plain callables — no FastAPI `Depends` syntax in user code
@@ -0,0 +1,14 @@
1
+ vs_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ vs_server/decorator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ vs_server/decorator/vs_controller_decorator.py,sha256=NaHEAsojE48bYF5aR9-QaZbDh-78047mtAfwGTFm3gQ,1238
4
+ vs_server/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ vs_server/schema/exceptions.py,sha256=FFU5KbgDfWUAOP8xSBdS9GpsSfh_ZXebE5563K2RzQk,4004
6
+ vs_server/schema/vs_server_health.py,sha256=C1mifay2x_ksxcTrtPH86K4J9xJ5D0Rl10iUB0EhksA,420
7
+ vs_server/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ vs_server/server/vs_fast_api_server.py,sha256=4Mr5Wqe8MtgVFZU9R4xmj6GQ5uQqQsRFowfKYAUEZIg,6817
9
+ vs_server/server/vs_server.py,sha256=vN-GJ37Zox5YPxKJOxSzatAgkwAMe_YkVZ8nseJHkxA,1707
10
+ vs_server-0.1.0.dist-info/licenses/LICENSE.txt,sha256=mqpzH4elTtvB1De7k3lbi79etbvNJ_7qS42EvG_hIJM,10123
11
+ vs_server-0.1.0.dist-info/METADATA,sha256=jGQOddRqRHU9dIhrxTcGadx6guv8n22LkZHFJHnmA08,26749
12
+ vs_server-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
13
+ vs_server-0.1.0.dist-info/top_level.txt,sha256=-ihR9XRPABHwQqCOcY-YpoqKLkmzT1YMVlc7hTt2dB8,10
14
+ vs_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,181 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship made available under
36
+ the License, as indicated by a copyright notice that is included in
37
+ or attached to the work (an example is provided in the Appendix below).
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship. For the purposes
43
+ of this License, Derivative Works shall not include works that remain
44
+ separable from, or merely link (or bind by name) to the interfaces of,
45
+ the Work and derivative works thereof.
46
+
47
+ "Contribution" shall mean, as submitted to the Licensor for inclusion
48
+ in the Work by the copyright owner or by an individual or Legal Entity
49
+ authorized to submit on behalf of the copyright owner. For the purposes
50
+ of this definition, "submitted" means any form of electronic, verbal,
51
+ or written communication sent to the Licensor or its representatives,
52
+ including but not limited to communication on electronic mailing lists,
53
+ source code control systems, and issue tracking systems that are managed
54
+ by, or on behalf of, the Licensor for the purpose of discussing and
55
+ improving the Work, but excluding communication that is conspicuously
56
+ marked or designated in writing by the copyright owner as "Not a
57
+ Contribution."
58
+
59
+ "Contributor" shall mean Licensor and any Legal Entity on behalf of
60
+ whom a Contribution has been received by the Licensor and incorporated
61
+ within the Work.
62
+
63
+ 2. Grant of Copyright License. Subject to the terms and conditions of
64
+ this License, each Contributor hereby grants to You a perpetual,
65
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
66
+ copyright license to reproduce, prepare Derivative Works of,
67
+ publicly display, publicly perform, sublicense, and distribute the
68
+ Work and such Derivative Works in Source or Object form.
69
+
70
+ 3. Grant of Patent License. Subject to the terms and conditions of
71
+ this License, each Contributor hereby grants to You a perpetual,
72
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
73
+ (except as stated in this section) patent license to make, have made,
74
+ use, offer to sell, sell, import, and otherwise transfer the Work,
75
+ where such license applies only to those patent claims licensable
76
+ by such Contributor that are necessarily infringed by their
77
+ Contribution(s) alone or by the combination of their Contribution(s)
78
+ with the Work to which such Contribution(s) was submitted. If You
79
+ institute patent litigation against any entity (including a cross-claim
80
+ or counterclaim in a lawsuit) alleging that the Work or any
81
+ Contribution embodied within the Work constitutes direct or
82
+ contributory patent infringement, then any patent licenses granted to
83
+ You under this License for that Work shall terminate as of the date
84
+ such litigation is filed.
85
+
86
+ 4. Redistribution. You may reproduce and distribute copies of the
87
+ Work or Derivative Works thereof in any medium, with or without
88
+ modifications, and in Source or Object form, provided that You
89
+ meet the following conditions:
90
+
91
+ (a) You must give any other recipients of the Work or Derivative
92
+ Works a copy of this License; and
93
+
94
+ (b) You must cause any modified files to carry prominent notices
95
+ stating that You changed the files; and
96
+
97
+ (c) You must retain, in the Source form of any Derivative Works
98
+ that You distribute, all copyright, patent, trademark, and
99
+ attribution notices from the Source form of the Work,
100
+ excluding those notices that do not pertain to any part of
101
+ the Derivative Works; and
102
+
103
+ (d) If the Work includes a "NOTICE" file as part of its distribution,
104
+ You must include a readable copy of the attribution notices
105
+ contained within such NOTICE file, in at least one of the
106
+ following places: within a NOTICE text file distributed as part
107
+ of the Derivative Works; within the Source form or documentation,
108
+ if provided along with the Derivative Works; or, within a display
109
+ generated by the Derivative Works, if and wherever such
110
+ third-party notices normally appear. The contents of the NOTICE
111
+ file are for informational purposes only and do not modify the
112
+ License. You may add Your own attribution notices within
113
+ Derivative Works that You distribute, alongside or as an addendum
114
+ to the NOTICE text from the Work, provided that such additional
115
+ attribution notices cannot be construed as modifying the License.
116
+
117
+ You may add Your own license statement for Your modifications and
118
+ may provide additional grant of rights to use, modify, and distribute
119
+ Your modifications.
120
+
121
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
122
+ any Contribution intentionally submitted for inclusion in the Work
123
+ by You to the Licensor shall be under the terms and conditions of
124
+ this License, without any additional terms or conditions.
125
+ Notwithstanding the above, nothing herein shall supersede or modify
126
+ the terms of any separate license agreement you may have executed
127
+ with Licensor regarding such Contributions.
128
+
129
+ 6. Trademarks. This License does not grant permission to use the trade
130
+ names, trademarks, service marks, or product names of the Licensor,
131
+ except as required for reasonable and customary use in describing the
132
+ origin of the Work and reproducing the content of the NOTICE file.
133
+
134
+ 7. Disclaimer of Warranty. Unless required by applicable law or
135
+ agreed to in writing, Licensor provides the Work (and each
136
+ Contributor provides its Contributions) on an "AS IS" BASIS,
137
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
138
+ implied, including, without limitation, any conditions of TITLE,
139
+ NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
140
+ PURPOSE. You are solely responsible for determining the
141
+ appropriateness of using or reproducing the Work and assume any
142
+ risks associated with Your exercise of permissions under this License.
143
+
144
+ 8. Limitation of Liability. In no event and under no legal theory,
145
+ whether in tort (including negligence), contract, or otherwise,
146
+ unless required by applicable law (such as deliberate and grossly
147
+ negligent acts) or agreed to in writing, shall any Contributor be
148
+ liable to You for damages, including any direct, indirect, special,
149
+ incidental, or exemplary damages of any character arising as a
150
+ result of this License or out of the use or inability to use the
151
+ Work (including but not limited to damages for loss of goodwill,
152
+ work stoppage, computer failure or malfunction, or all other
153
+ commercial damages or losses), even if such Contributor has been
154
+ advised of the possibility of such damages.
155
+
156
+ 9. Accepting Warranty or Additional Liability. While redistributing
157
+ the Work or Derivative Works thereof, You may choose to offer,
158
+ and charge a fee for, acceptance of support, warranty, indemnity,
159
+ or other liability obligations and/or rights consistent with this
160
+ License. However, in accepting such obligations, You may act only
161
+ on Your own behalf and on Your sole responsibility, not on behalf
162
+ of any other Contributor, and only if You agree to indemnify,
163
+ defend, and hold each Contributor harmless for any liability
164
+ incurred by, or claims asserted against, such Contributor by reason
165
+ of your accepting any such warranty or additional liability.
166
+
167
+ END OF TERMS AND CONDITIONS
168
+
169
+ Copyright 2026 VivekaSutra. All rights reserved.
170
+
171
+ Licensed under the Apache License, Version 2.0 (the "License");
172
+ you may not use this file except in compliance with the License.
173
+ You may obtain a copy of the License at
174
+
175
+ http://www.apache.org/licenses/LICENSE-2.0
176
+
177
+ Unless required by applicable law or agreed to in writing, software
178
+ distributed under the License is distributed on an "AS IS" BASIS,
179
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
180
+ implied. See the License for the specific language governing
181
+ permissions and limitations under the License.
@@ -0,0 +1 @@
1
+ vs_server