melody-web 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- melody_web-0.1.1/PKG-INFO +11 -0
- melody_web-0.1.1/README.md +31 -0
- melody_web-0.1.1/melody/__init__.py +5 -0
- melody_web-0.1.1/melody/application.py +66 -0
- melody_web-0.1.1/melody/cli.py +27 -0
- melody_web-0.1.1/melody/cookie.py +39 -0
- melody_web-0.1.1/melody/db/__init__.py +1 -0
- melody_web-0.1.1/melody/db/table.py +26 -0
- melody_web-0.1.1/melody/environment.py +7 -0
- melody_web-0.1.1/melody/render.py +7 -0
- melody_web-0.1.1/melody/request.py +19 -0
- melody_web-0.1.1/melody/response.py +37 -0
- melody_web-0.1.1/melody/server.py +9 -0
- melody_web-0.1.1/melody_web.egg-info/PKG-INFO +11 -0
- melody_web-0.1.1/melody_web.egg-info/SOURCES.txt +19 -0
- melody_web-0.1.1/melody_web.egg-info/dependency_links.txt +1 -0
- melody_web-0.1.1/melody_web.egg-info/entry_points.txt +2 -0
- melody_web-0.1.1/melody_web.egg-info/requires.txt +6 -0
- melody_web-0.1.1/melody_web.egg-info/top_level.txt +1 -0
- melody_web-0.1.1/pyproject.toml +21 -0
- melody_web-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: melody-web
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Python web framework to quickly build web apps
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: uvicorn
|
|
7
|
+
Requires-Dist: werkzeug
|
|
8
|
+
Requires-Dist: beautifulsoup4
|
|
9
|
+
Requires-Dist: Jinja2
|
|
10
|
+
Requires-Dist: python-dotenv
|
|
11
|
+
Requires-Dist: peewee
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Melody
|
|
2
|
+
|
|
3
|
+
Melody is a python based web framework designed to let users build websites as fast as possible, with almost no boilerplate code.
|
|
4
|
+
|
|
5
|
+
Till now, I have built-
|
|
6
|
+
1. A Response and Request Class
|
|
7
|
+
2. A very basic CLI to run and deploy the apps
|
|
8
|
+
3. Dynamic routing with slugs (built on Werkzeug)
|
|
9
|
+
4. Reading environment variables
|
|
10
|
+
5. A basic sqlite3 ORM (built on peewee orm)
|
|
11
|
+
|
|
12
|
+
## 1. How to build a simple app
|
|
13
|
+
|
|
14
|
+
You first need to install melody by
|
|
15
|
+
```bash
|
|
16
|
+
pip install melody
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# routes.py
|
|
21
|
+
from .controllers import *
|
|
22
|
+
|
|
23
|
+
routes = [
|
|
24
|
+
'/': index,
|
|
25
|
+
'POST /new': new
|
|
26
|
+
]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is the basic snippet for running the code. You can either create a new file for storing the controllers, or define them in the routes.py itself.
|
|
30
|
+
|
|
31
|
+
To run your app, you can
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import importlib
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
from melody.request import Request
|
|
5
|
+
from .response import parse_response
|
|
6
|
+
|
|
7
|
+
routes_import = importlib.import_module('routes')
|
|
8
|
+
print(routes_import.routes)
|
|
9
|
+
routes = routes_import.routes
|
|
10
|
+
|
|
11
|
+
from werkzeug.routing import Map, Rule
|
|
12
|
+
|
|
13
|
+
route_map = Map([
|
|
14
|
+
Rule(route.split(' ')[1] if ' ' in route else route, endpoint= route, methods=[route.split(' ')[0]] if ' ' in route else None) for route in routes.keys()
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
def is_html(text):
|
|
18
|
+
return bool(BeautifulSoup(text, "html.parser").find())
|
|
19
|
+
|
|
20
|
+
async def app(scope, receive, send):
|
|
21
|
+
adapter = route_map.bind('http://127.0.0.1:8000/')
|
|
22
|
+
if scope['type'] != 'http':
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
path = scope['path']
|
|
26
|
+
try:
|
|
27
|
+
endpoint, params = adapter.match(path, method=scope['method'])
|
|
28
|
+
except:
|
|
29
|
+
await send({
|
|
30
|
+
"type": "http.response.start",
|
|
31
|
+
"status": 404,
|
|
32
|
+
"headers": [
|
|
33
|
+
[b"content-type", b"text/plain"],
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await send({
|
|
38
|
+
"type": "http.response.body",
|
|
39
|
+
"body": b"Not Found",
|
|
40
|
+
})
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
data = routes[endpoint](Request(scope, params))
|
|
44
|
+
res_data, res_type, status, res_headers, cookies = parse_response(data)
|
|
45
|
+
headers = [[b"content-type", res_type]]
|
|
46
|
+
print('res', res_headers)
|
|
47
|
+
|
|
48
|
+
for key, value in res_headers.items():
|
|
49
|
+
print('p', key)
|
|
50
|
+
headers.append([key.lower().encode(), str(value).encode()])
|
|
51
|
+
|
|
52
|
+
for value in cookies:
|
|
53
|
+
headers.append(['Set-Cookie', value])
|
|
54
|
+
|
|
55
|
+
await send({
|
|
56
|
+
"type": "http.response.start",
|
|
57
|
+
"status": status,
|
|
58
|
+
"headers": headers
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await send({
|
|
62
|
+
"type": "http.response.body",
|
|
63
|
+
"body": res_data,
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
root = Path.cwd()
|
|
5
|
+
sys.path.insert(0, str(root))
|
|
6
|
+
# :this: it tells the application.py to look for routes.py in the cwd of where the cmd was executed
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
command = sys.argv[1] if len(sys.argv) > 1 else None
|
|
10
|
+
|
|
11
|
+
port = 8000
|
|
12
|
+
|
|
13
|
+
if "--port" in sys.argv:
|
|
14
|
+
index = sys.argv.index("--port")
|
|
15
|
+
port = int(sys.argv[index + 1])
|
|
16
|
+
|
|
17
|
+
if command == 'run':
|
|
18
|
+
from melody.server import run
|
|
19
|
+
run(port=port)
|
|
20
|
+
if command == 'deploy':
|
|
21
|
+
from melody.server import run
|
|
22
|
+
run(port=port, reload=False)
|
|
23
|
+
# if sys.argv[1] == 'new':
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == '__main__':
|
|
27
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
def cookie(
|
|
2
|
+
name,
|
|
3
|
+
value,
|
|
4
|
+
max_age=None,
|
|
5
|
+
expires=None,
|
|
6
|
+
domain=None,
|
|
7
|
+
path="/",
|
|
8
|
+
secure=False,
|
|
9
|
+
httponly=False,
|
|
10
|
+
samesite=None,
|
|
11
|
+
partitioned=False,
|
|
12
|
+
):
|
|
13
|
+
parts = [f"{name}={value}"]
|
|
14
|
+
|
|
15
|
+
if max_age is not None:
|
|
16
|
+
parts.append(f"Max-Age={max_age}")
|
|
17
|
+
|
|
18
|
+
if expires is not None:
|
|
19
|
+
parts.append(f"Expires={expires}")
|
|
20
|
+
|
|
21
|
+
if domain is not None:
|
|
22
|
+
parts.append(f"Domain={domain}")
|
|
23
|
+
|
|
24
|
+
if path is not None:
|
|
25
|
+
parts.append(f"Path={path}")
|
|
26
|
+
|
|
27
|
+
if secure:
|
|
28
|
+
parts.append("Secure")
|
|
29
|
+
|
|
30
|
+
if httponly:
|
|
31
|
+
parts.append("HttpOnly")
|
|
32
|
+
|
|
33
|
+
if samesite is not None:
|
|
34
|
+
parts.append(f"SameSite={samesite}")
|
|
35
|
+
|
|
36
|
+
if partitioned:
|
|
37
|
+
parts.append("Partitioned")
|
|
38
|
+
|
|
39
|
+
return "; ".join(parts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .table import Database, Table, CharField, IntegerField, BooleanField
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from peewee import ModelBase, SqliteDatabase, Model, CharField, IntegerField, BooleanField
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Database:
|
|
5
|
+
current = None
|
|
6
|
+
|
|
7
|
+
def __init__(self, name="sqlite3.db"):
|
|
8
|
+
self.db = SqliteDatabase(name)
|
|
9
|
+
Database.current = self
|
|
10
|
+
|
|
11
|
+
def register(self, tables):
|
|
12
|
+
self.db.create_tables(tables)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TableMeta(ModelBase):
|
|
16
|
+
def __new__(cls, name, bases, attrs):
|
|
17
|
+
table = super().__new__(cls, name, bases, attrs)
|
|
18
|
+
|
|
19
|
+
if Database.current:
|
|
20
|
+
table._meta.database = Database.current.db
|
|
21
|
+
|
|
22
|
+
return table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Table(Model, metaclass=TableMeta):
|
|
26
|
+
pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from urllib.parse import parse_qs
|
|
2
|
+
|
|
3
|
+
class Request:
|
|
4
|
+
def __init__(self, scope, params):
|
|
5
|
+
print(scope)
|
|
6
|
+
self.path = scope['path']
|
|
7
|
+
self.method = scope['method']
|
|
8
|
+
self.query = parse_qs(scope["query_string"].decode('utf-8'))
|
|
9
|
+
self.headers = {key.decode('latin-1'): value.decode('latin-1') for key, value in scope["headers"]}
|
|
10
|
+
self.params = params
|
|
11
|
+
self.scope = scope
|
|
12
|
+
self.cookies = {}
|
|
13
|
+
|
|
14
|
+
for cookie in self.headers.get('cookie', '').split(';'):
|
|
15
|
+
if cookie == '':
|
|
16
|
+
continue
|
|
17
|
+
print('cookie', cookie)
|
|
18
|
+
key, value = cookie.strip().split('=')
|
|
19
|
+
self.cookies[key] = value
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from bs4 import BeautifulSoup
|
|
3
|
+
|
|
4
|
+
def is_html(text):
|
|
5
|
+
return bool(BeautifulSoup(text, "html.parser").find())
|
|
6
|
+
|
|
7
|
+
class Response:
|
|
8
|
+
def __init__(self, data, response_type=None, status=200, headers={}, cookies={}):
|
|
9
|
+
if response_type is not None:
|
|
10
|
+
self.response_type = str(response_type).encode()
|
|
11
|
+
self.response_data = (
|
|
12
|
+
data if isinstance(data, bytes)
|
|
13
|
+
else str(data).encode()
|
|
14
|
+
)
|
|
15
|
+
elif isinstance(data, dict):
|
|
16
|
+
self.response_data = json.dumps(data).encode()
|
|
17
|
+
self.response_type = b"application/json"
|
|
18
|
+
elif is_html(data):
|
|
19
|
+
self.response_data = str(data).encode()
|
|
20
|
+
self.response_type=b"text/html"
|
|
21
|
+
else:
|
|
22
|
+
self.response_data = str(data).encode()
|
|
23
|
+
self.response_type = b"text/plain"
|
|
24
|
+
self.status = status
|
|
25
|
+
self.headers = headers
|
|
26
|
+
self.cookies = cookies
|
|
27
|
+
|
|
28
|
+
def parse_response(response):
|
|
29
|
+
print('type', type(response))
|
|
30
|
+
if isinstance(response, Response):
|
|
31
|
+
return response.response_data, response.response_type, response.status, response.headers, response.cookies
|
|
32
|
+
temp_response = Response(response)
|
|
33
|
+
print('t', temp_response.cookies)
|
|
34
|
+
return temp_response.response_data, temp_response.response_type, temp_response.status, temp_response.headers, temp_response.cookies
|
|
35
|
+
|
|
36
|
+
def redirect(path):
|
|
37
|
+
return Response('', status=302, headers={'Location': path})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: melody-web
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Python web framework to quickly build web apps
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: uvicorn
|
|
7
|
+
Requires-Dist: werkzeug
|
|
8
|
+
Requires-Dist: beautifulsoup4
|
|
9
|
+
Requires-Dist: Jinja2
|
|
10
|
+
Requires-Dist: python-dotenv
|
|
11
|
+
Requires-Dist: peewee
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
melody/__init__.py
|
|
4
|
+
melody/application.py
|
|
5
|
+
melody/cli.py
|
|
6
|
+
melody/cookie.py
|
|
7
|
+
melody/environment.py
|
|
8
|
+
melody/render.py
|
|
9
|
+
melody/request.py
|
|
10
|
+
melody/response.py
|
|
11
|
+
melody/server.py
|
|
12
|
+
melody/db/__init__.py
|
|
13
|
+
melody/db/table.py
|
|
14
|
+
melody_web.egg-info/PKG-INFO
|
|
15
|
+
melody_web.egg-info/SOURCES.txt
|
|
16
|
+
melody_web.egg-info/dependency_links.txt
|
|
17
|
+
melody_web.egg-info/entry_points.txt
|
|
18
|
+
melody_web.egg-info/requires.txt
|
|
19
|
+
melody_web.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
melody
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "melody-web"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "A Python web framework to quickly build web apps"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"uvicorn",
|
|
13
|
+
"werkzeug",
|
|
14
|
+
"beautifulsoup4",
|
|
15
|
+
"Jinja2",
|
|
16
|
+
"python-dotenv",
|
|
17
|
+
"peewee",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
melody = "melody.cli:main"
|