├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.MD ├── README.md ├── lumi ├── __init__.py ├── api.py ├── enums.py ├── helpers.py └── server.py ├── requirements-dev.txt ├── requirements.txt └── setup.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | merge_group: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | contrib-readme-job: 11 | runs-on: ubuntu-latest 12 | name: A job to automate contrib in readme 13 | steps: 14 | - name: Contribute List 15 | uses: akhilmhdh/contributors-readme-action@v2.3.6 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | test.py 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | 163 | test.py -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Tanmoy Sarkar 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lumi 💧 2 | 3 | 4 | 5 | Lumi is a nano framework to convert your python functions into a REST API without any extra headache. 6 | 7 | * This library is created by taking the concept of **RPC** and blended with **REST API** specs. 8 | * We need to just register the function and it will be available as a REST API. 9 | * Web-server written with **Gunicorn** 10 | * Local development server provided for rapid development and prototyping. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install lumi 16 | ``` 17 | 18 | ## Function <--> API mapping 19 | ![function - API mapping](https://raw.githubusercontent.com/Tanmoy741127/cdn/main/lumi/function-api-map.png) 20 | 21 | 22 | ## How to use 🤔 23 | 24 | Let's create a simple function to add two numbers. 25 | 26 | ```python 27 | def add(a, b): 28 | return a + b 29 | 30 | def subtract(a, b): 31 | return a - b 32 | ``` 33 | 34 | Now, we want to expose this function as a REST API. We can do this by registering the function with Lumi. 35 | 36 | ```python 37 | # app.py 38 | 39 | from lumi import Lumi 40 | 41 | app = Lumi() 42 | 43 | app.register(add) # Registering the function 44 | app.register(subtract) 45 | 46 | app.runServer(host="127.0.0.1", port=8080) 47 | ``` 48 | 49 | Noice 🎉🎉 API has been generated 50 | 51 | Run the sever by 52 | ``` 53 | python app.py 54 | ``` 55 | You are going to see this in your terminal 56 | ``` 57 | [2022-11-24 17:32:08 +0530] [10490] [INFO] Starting gunicorn 20.1.0 58 | [2022-11-24 17:32:08 +0530] [10490] [INFO] Listening at: http://127.0.0.1:8080 (10490) 59 | [2022-11-24 17:32:08 +0530] [10490] [INFO] Using worker: sync 60 | [2022-11-24 17:32:08 +0530] [10492] [INFO] Booting worker with pid: 10492 61 | ... 62 | ... 63 | [2022-11-24 17:32:08 +0530] [10500] [INFO] Booting worker with pid: 10500 64 | ``` 65 | 66 | Congratulations 👏. Our Server is online. 67 | 68 | 69 | The above code will generate a REST API with the following details. 70 | 71 | - Endpoint : `127.0.0.1:8080` 72 | - Route : `/add` 73 | - Method : `POST` 74 | - Sample Request Body : `{"a": 1, "b": 2}` 75 | 76 | Let's run the API and test it. 77 | 78 | ```curl 79 | curl -X POST -H "Content-Type: application/json" -d '{"a": 1, "b": 2}' http://127.0.0.1:8080/add 80 | ``` 81 | 82 | Output 83 | 84 | ```json 85 | { 86 | "exit_code": 0, 87 | "status_code": 200, 88 | "result": 3, 89 | "error": "" 90 | } 91 | ``` 92 | 93 | ## Custom Routing 94 | Now you may think, the function name will be always same as the route. But, you can change the route by passing the route parameter. 95 | 96 | ```python 97 | app.register(add, route="/addition") 98 | ``` 99 | ## Custom Request Method 100 | By default, the request method is `POST`. But, you can change it by passing the method parameter. Currently, it supports `GET`, `POST`, `PUT` and `PATCH` methods. 101 | 102 | ```python 103 | from lumi import Lumi, RequestMethod 104 | 105 | app = Lumi() 106 | 107 | def add(a, b): 108 | return a+b 109 | 110 | # Default : Register function for POST method 111 | app.register(add) 112 | # Register function for GET method 113 | app.register(add, request_method=RequestMethod.GET) 114 | # Register function for POST method 115 | app.register(add, request_method=RequestMethod.POST) 116 | # Register function for PUT method 117 | app.register(add, request_method=RequestMethod.PUT) 118 | # Register function for PATCH method 119 | app.register(add, request_method=RequestMethod.PATCH) 120 | 121 | app.runServer() 122 | ``` 123 | 124 | 🟡 **Pay attention before using GET request :** If you are using `GET` method 125 | - You need to pass the parameters in the query string, as `GET` dont support request body. 126 | - All those arguments, that will be passed to function will be in **String** format. So take care to convert them to the desired type in your function. 127 | 128 | 129 | ## Send File 130 | Send file to user by returning the file object. 131 | 132 | ```python 133 | from lumi import Lumi, RequestMethod 134 | app = Lumi() 135 | 136 | def download_file(): 137 | return open("file.txt", "rb") # Return file object 138 | 139 | app.register(download_file) 140 | ``` 141 | 142 | ## Debug Mode 143 | By default, the debug mode is `True`. But, you can change it by passing the debug parameter. 144 | 145 | ```python 146 | # app.py 147 | 148 | from lumi import Lumi 149 | 150 | app = Lumi(debug=False) 151 | ... 152 | ``` 153 | 154 | ## Status Codes 155 | 156 | | Status Code | Description | 157 | | --- | --- | 158 | | 200 | Request successfully executed and No Error happened during function execution | 159 | | 500 | Request was received but there was an error during function execution | 160 | | 400 | Bad Request (Possible Reason - The required parameters for the function has not provided) | 161 | | 405 | Method Not Allowed (Lumi only supports **POST** request) | 162 | | 404 | The route has no function associated with that | 163 | 164 | 165 | ## Exit Codes 166 | | Exit Code | Description | 167 | | --- | --- | 168 | | 0 | No Error | 169 | | 1 | Error | 170 | 171 | > Note : If the function has some error , you can expect the exit code to be 1 and the error message in the response. 172 | 173 | ## Task Lists 174 | - [x] Base System 175 | - [x] Add support for default parameters that is provided in the function 176 | - [x] Debug mode and logging support 177 | - [x] Make available GET request for the function 178 | - [x] Provide option to override POST with PUT if the user wants 179 | - [x] Add support to send file directly to user 180 | - [ ] Add support to serve files through a public folder [Customizable] 181 | - [ ] Add suport for middleware integration 182 | - [ ] Support nested routing of urls 183 | - [ ] For local development, create an file observer that can automatically reload the server when the file is changed. 184 | - [ ] Add support for object serialization and deserialization based on argument types of function 185 | 186 | ## Contributing 187 | 188 | Contributions are always welcome! 189 | ## Our community 190 | 191 | 192 | 193 | 194 | 201 | 208 | 215 | 222 |
195 | 196 | Tanmoy741127 197 |
198 | Tanmoy Sarkar 199 |
200 |
202 | 203 | AmirMGhanem 204 |
205 | Amir M. Ghanem 206 |
207 |
209 | 210 | matheusfelipeog 211 |
212 | Matheus Felipe 213 |
214 |
216 | 217 | 0xflotus 218 |
219 | 0xflotus 220 |
221 |
223 | 224 | 225 | ## Support 226 | Buy Me A Coffee 227 | -------------------------------------------------------------------------------- /lumi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lumi is a nano framework to convert your python functions 3 | into a REST API without any extra headache. 4 | """ 5 | 6 | from lumi.api import Lumi 7 | from lumi.enums import RequestMethod 8 | -------------------------------------------------------------------------------- /lumi/api.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from nanoid import generate 3 | import json 4 | import io 5 | import os 6 | 7 | from lumi.server import DevelopmentServer 8 | from lumi.enums import RequestMethod 9 | from lumi.helpers import parseQueryParameter 10 | 11 | class Lumi: 12 | instance = None 13 | 14 | @staticmethod 15 | def getInstance(): 16 | if Lumi.instance is None: 17 | Lumi.instance = Lumi() 18 | return Lumi.instance 19 | 20 | def __init__(self, debug=True): 21 | self.registered_functions = {} 22 | self.function_routing_map = { 23 | RequestMethod.GET: {}, 24 | RequestMethod.POST : {}, 25 | RequestMethod.PUT : {}, 26 | RequestMethod.PATCH : {} 27 | } 28 | ''' 29 | This dictionary will store the route along with request type and the function metadata 30 | Function metadata will have functionKey . 31 | With the functionKey we can get the function from the registered_functions dictionary 32 | ''' 33 | 34 | self.debug = debug # Make it false, if you are in production 35 | 36 | def register(self, function, route:str=None, request_method=RequestMethod.POST)->None: 37 | functionKey = generate(size=10) 38 | 39 | # Store the function in the registered_functions dictionary 40 | self.registered_functions[functionKey] = function 41 | 42 | # Function name 43 | name = function.__code__.co_name 44 | module_name = function.__module__ 45 | file_name = function.__code__.co_filename 46 | 47 | # Generate function metadata and store it in the function_routing_map 48 | no_of_arguments = function.__code__.co_argcount 49 | function_parameters = list(function.__code__.co_varnames)[:no_of_arguments] 50 | default_parameters = function.__defaults__ 51 | default_parameters = list(default_parameters) if default_parameters is not None else [] 52 | 53 | # Calculate no of parameters 54 | no_of_function_parameters = len(function_parameters) 55 | no_of_default_parameters = len(default_parameters) 56 | no_of_required_parameters = no_of_function_parameters - no_of_default_parameters 57 | 58 | # Calculate the required parameters and optional parameters 59 | required_parameters = function_parameters[:no_of_required_parameters] 60 | optional_parameters = function_parameters[no_of_required_parameters:no_of_function_parameters] 61 | 62 | # Create default parameters dictionary 63 | default_parameters_map = {} 64 | for i in range(len(optional_parameters)): 65 | default_parameters_map[optional_parameters[i]] = default_parameters[i] 66 | 67 | # Key for Function Routing Map 68 | function_routing_map_key = name if route is None else route 69 | 70 | # Add / if not at the start 71 | if function_routing_map_key.startswith("/") is False: 72 | function_routing_map_key = '/' + function_routing_map_key 73 | 74 | # Remove / if at the end 75 | if function_routing_map_key.endswith("/") is True: 76 | function_routing_map_key = function_routing_map_key[:-1] 77 | 78 | 79 | self.function_routing_map[request_method][function_routing_map_key] = { 80 | "name": name, 81 | "module_name": module_name, 82 | "file_name": file_name, 83 | "key": functionKey, 84 | "parameters": { 85 | "all": function_parameters, 86 | "required": required_parameters, 87 | "optional": optional_parameters 88 | }, 89 | "default_values": default_parameters_map 90 | } 91 | 92 | def wsgi_app(self, environ:dict, start_response:typing.Callable): 93 | method = environ.get("REQUEST_METHOD","") 94 | content_type = environ.get("CONTENT_TYPE", "") 95 | route = environ.get("PATH_INFO", "") 96 | 97 | ## Block all the methods except GET, POST, PUT and PATCH 98 | if method != RequestMethod.GET and method != RequestMethod.POST and method != RequestMethod.PUT and method != RequestMethod.PATCH: 99 | start_response("405 Method Not Allowed", [('Content-Type', 'application/json')]) 100 | if self.debug: 101 | print("[%s] [%s] %s" % (method, 405, route)) 102 | return [b'{"exit_code": 1, "status_code": 405, "result": "", "error": "Method Not Allowed"}'] 103 | 104 | ## Check content type 105 | # If other than application/json, return 415 Unsupported Media Type 106 | if content_type != "application/json" and method in [RequestMethod.POST, RequestMethod.PATCH, RequestMethod.PUT]: 107 | start_response("415 Unsupported Media Type", [('Content-Type', 'application/json')]) 108 | if self.debug: 109 | print("[%s] [%s] %s" % (method, 425, route)) 110 | return [b'{"exit_code": 1, "status_code": 415, "result": "", "error": "Unsupported Media Type"}'] 111 | 112 | 113 | ## If route is not in the function_routing_map, return 404 Not Found 114 | if route not in self.function_routing_map[method]: 115 | start_response("404 Not Found", [('Content-Type', 'application/json')]) 116 | if self.debug: 117 | print("[%s] [%s] %s" % (method, 404, route)) 118 | return [b'{"exit_code": 1, "status_code": 404, "result": "", "error": "Not Found"}'] 119 | 120 | # Body of the request 121 | raw_body = environ["wsgi.input"].read() 122 | if raw_body is None or raw_body == b"" or raw_body == "": 123 | # Maybe the request is not having any body, [Possible reason : Function needs no parameters] 124 | # So, we will pass an empty dictionary 125 | raw_body = "{}" 126 | 127 | ## Parse body of POST, PUT, PATCH 128 | request_body = None 129 | try: 130 | request_body = json.loads(raw_body) 131 | except : 132 | # If there is any error parsing the body of the request, return 400 Bad Request 133 | start_response("400 Bad Request", [('Content-Type', 'application/json')]) 134 | if self.debug: 135 | print("[%s] [%s] %s" % (method, 400, route)) 136 | return [b'{"exit_code": 1, "status_code": 400, "result": "", "error": "Failed to decode JSON"}'] 137 | 138 | ## Parse data from query parameters in case of GET request 139 | if method == RequestMethod.GET: 140 | request_body = parseQueryParameter(environ["QUERY_STRING"]) 141 | 142 | # Get the function metadata 143 | function_metadata = self.function_routing_map[method][route] 144 | function_object = self.registered_functions[function_metadata["key"]] 145 | 146 | ## Serialize the arguments 147 | arguments = [] 148 | # Check if all the required parameters are present in the request body 149 | for parameter in function_metadata["parameters"]["required"]: 150 | if parameter in request_body: 151 | # If present, add it to the arguments list 152 | arguments.append(request_body[parameter]) 153 | else: 154 | # If any of the required parameters are not present, return 400 Bad Request 155 | start_response("400 Bad Request", [('Content-Type', 'application/json')]) 156 | if self.debug: 157 | print("[%s] [%s] %s" % (method, 400, route)) 158 | return [b"400 Bad Request"] 159 | 160 | # Check if any of the optional parameters are present in the request body 161 | for parameter in function_metadata["parameters"]["optional"]: 162 | if parameter in request_body: 163 | # If present, add it to the arguments list 164 | arguments.append(request_body[parameter]) 165 | else: 166 | # If not present, add the default value to the arguments list 167 | arguments.append(function_metadata["default_values"][parameter]) 168 | 169 | 170 | result = None 171 | error = None 172 | status_code = 200 173 | exit_code = 0 174 | isFile = False 175 | 176 | try: 177 | result = function_object(*arguments) 178 | isFile = isinstance(result, io.IOBase) 179 | status_code = 200 180 | exit_code = 0 181 | except FileNotFoundError as e: 182 | error = str(e.strerror) 183 | status_code = 404 184 | exit_code = 1 185 | except Exception as e: 186 | error = str(e) 187 | status_code = 500 188 | exit_code = 1 189 | 190 | ## Serve Requests 191 | if isFile: 192 | # Send the file 193 | filename_with_path = result.name or 'unnamed' 194 | # Check if the result is instance of TextIOWrapper 195 | isTextIOWrapped = isinstance(result, io.TextIOWrapper) 196 | if isTextIOWrapped: 197 | result = io.open(filename_with_path, mode='rb') 198 | 199 | start_response("200 OK", [('Content-Disposition', f'attachment; filename={os.path.basename(filename_with_path)}')]) 200 | if self.debug: 201 | print("[%s] [%s] %s" % (method, "200", route)) 202 | if 'wsgi.file_wrapper' in environ: 203 | return environ['wsgi.file_wrapper'](result, os.path.getsize(filename_with_path)) 204 | else: 205 | return iter(lambda: result.read(os.path.getsize(filename_with_path)), '') 206 | else: 207 | # All responses except file 208 | response = { 209 | "exit_code": exit_code, 210 | "status_code": status_code, 211 | "result": result if result is not None else "", 212 | "error": error if error is not None else "" 213 | } 214 | 215 | status_text = "200 OK" if status_code == 200 else f"{str(status_code)} Internal Server Error" 216 | start_response(status_text, [('Content-Type', 'application/json')]) 217 | if self.debug: 218 | print("[%s] [%s] %s" % (method, status_code, route)) 219 | return iter([json.dumps(response).encode()]) 220 | 221 | def print_registered_functions(self): 222 | import json 223 | print(json.dumps(self.function_routing_map)) 224 | 225 | def runServer(self, host="127.0.0.1", port=8080, threads:int=4): 226 | options = { 227 | 'listen': '%s:%s' % (host, str(port)), 228 | 'threads': threads, 229 | } 230 | devServer = DevelopmentServer(self, options) 231 | devServer.run() 232 | 233 | def __call__(self, environ:dict, start_response: typing.Callable) -> typing.Any: 234 | return self.wsgi_app(environ, start_response) 235 | -------------------------------------------------------------------------------- /lumi/enums.py: -------------------------------------------------------------------------------- 1 | class RequestMethod: 2 | GET = 'GET' 3 | POST = 'POST' 4 | PUT = 'PUT' 5 | PATCH = 'PATCH' -------------------------------------------------------------------------------- /lumi/helpers.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | 3 | def parseQueryParameter(query:str) -> dict: 4 | data = {} 5 | try: 6 | for record in parse.parse_qsl(query): 7 | data[record[0]] = record[1] 8 | except: 9 | print("[ERROR] failed to decode query string") 10 | print("Query -> ", query or "") 11 | return data -------------------------------------------------------------------------------- /lumi/server.py: -------------------------------------------------------------------------------- 1 | from waitress import serve 2 | 3 | ''' 4 | Development server for RPC . Used waitress WSGI server. 5 | In production, use gunicorn daemon to manage the server. 6 | ''' 7 | class DevelopmentServer: 8 | def __init__(self, app, options=None): 9 | self.options = options or {} 10 | self.application = app 11 | super().__init__() 12 | 13 | def load_config(self): 14 | config = {key: value for key, value in self.options.items() 15 | if key in self.cfg.settings and value is not None} 16 | for key, value in config.items(): 17 | self.cfg.set(key.lower(), value) 18 | 19 | def run(self): 20 | try: 21 | print("🚀 Running development server at http://%s" % self.options["listen"]) 22 | return serve(self.application, listen=self.options["listen"], threads=self.options["threads"]) 23 | except KeyboardInterrupt: 24 | print("Shutting down server.") 25 | 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | nanoid==2.0.0 2 | twine==4.0.1 3 | waitress==2.1.2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nanoid==2.0.0 2 | waitress==2.1.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='lumi', 8 | packages=find_packages(), 9 | version='1.0.11', 10 | description='Convert your Python functions into REST API without any extra effort 🔥', 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | author='Tanmoy Sarkar', 14 | author_email='ts741127@gmail.com', 15 | license='BSD', 16 | url="https://github.com/Tanmoy741127/lumi", 17 | classifiers=[ 18 | 'Development Status :: 4 - Beta', 19 | 'Intended Audience :: Developers', 20 | 'Topic :: Software Development :: Libraries :: Python Modules', 21 | 'License :: OSI Approved :: BSD License', 22 | 'Programming Language :: Python :: 3', 23 | ], 24 | keywords='rpc rest api web backend framework', 25 | python_requires='>=3.6', 26 | install_requires=[ 27 | "nanoid==2.0.0", 28 | "waitress==2.1.2" 29 | ], 30 | 31 | ) 32 | --------------------------------------------------------------------------------