init commit
This commit is contained in:
commit
439c894b39
37 changed files with 18468 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
|
||||
*.sublime-workspace
|
||||
|
||||
env/
|
||||
|
||||
metrics.json
|
||||
keys.json
|
||||
|
||||
# Need to do this in addition to adding to .gitignore
|
||||
# in order to fully ignore it.
|
||||
# git rm -r --cached conf.yml
|
||||
54
README.md
Normal file
54
README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Agent Framework
|
||||
|
||||
Fullstack framework tailored for agent workflows
|
||||
|
||||
Features include:
|
||||
|
||||
Document tools and models with new docstring protocol
|
||||
|
||||
doc.py, crud.py, order.py
|
||||
|
||||
Auto dependency injection (no enforced repo structure)
|
||||
|
||||
link.py, serve.py
|
||||
|
||||
UI template definition along side models
|
||||
|
||||
order.py
|
||||
|
||||
General comments on design:
|
||||
|
||||
Requests come in at serve.py
|
||||
|
||||
They are routed to server depending on subdomain (route.py)
|
||||
|
||||
Server routes are collected on boot and available globally
|
||||
|
||||
Repo structure is flexible which is great for dev satisfaction
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
#### Creates a virtual environment
|
||||
```
|
||||
python3 -m venv env
|
||||
```
|
||||
|
||||
#### Activates environment
|
||||
```
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
#### Install dependencies
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Run
|
||||
```
|
||||
cd src/
|
||||
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
See it build various routes, runners, and handlers, as well as start the webserver
|
||||
10
conf.yml
Normal file
10
conf.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
db:
|
||||
user: root
|
||||
database: crimata
|
||||
cors:
|
||||
Access-Control-Allow-Origin: "*"
|
||||
Access-Control-Allow-Methods: "*"
|
||||
Access-Control-Allow-Headers: "*"
|
||||
port: 8080
|
||||
public: "ui"
|
||||
12
requirements.txt
Normal file
12
requirements.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.12.15
|
||||
aiosignal==1.4.0
|
||||
attrs==25.3.0
|
||||
frozenlist==1.7.0
|
||||
idna==3.10
|
||||
multidict==6.6.4
|
||||
peewee==3.18.2
|
||||
propcache==0.3.2
|
||||
PyMySQL==1.1.1
|
||||
PyYAML==6.0.2
|
||||
yarl==1.20.1
|
||||
40
src/api.py
Normal file
40
src/api.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
$ routes
|
||||
|
||||
The collection of routes for the API. It gets
|
||||
|
||||
populated after all modules are imported.
|
||||
|
||||
"""
|
||||
routes = {}
|
||||
|
||||
|
||||
def create_route(func):
|
||||
|
||||
def wrapper(hello, *args, **kwargs):
|
||||
|
||||
res = func(*args, **kwargs)
|
||||
|
||||
return res
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def make_routes():
|
||||
|
||||
for name, note in notes.items():
|
||||
|
||||
for tag in doc.read_tags(note):
|
||||
|
||||
if tag == "route":
|
||||
|
||||
func = GET_LIB_REF(name)
|
||||
|
||||
route = create_route(func)
|
||||
|
||||
routes[name] = route
|
||||
|
||||
print(f"Routes: {routes.keys()}")
|
||||
55
src/card.py
Normal file
55
src/card.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from lib import *
|
||||
|
||||
"""
|
||||
𝌏 Card
|
||||
|
||||
A credit card model \
|
||||
|
||||
`
|
||||
[number]
|
||||
|
||||
[exp] [sec]
|
||||
|
||||
[name]
|
||||
|
||||
[street]
|
||||
|
||||
[city]
|
||||
|
||||
`
|
||||
> name [CharField] Cardholder's name \
|
||||
|
||||
> exp [CharField] Expiration date in "2035-12" format \
|
||||
|
||||
> sec [CharField] Authorize likes strings \
|
||||
|
||||
> street [CharField] Billing address street \
|
||||
|
||||
> city [CharField] City of billing address \
|
||||
|
||||
> account [IntField] ID of account for the card \
|
||||
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
𝑓 add_card
|
||||
|
||||
Add a credit card to an account for payment \
|
||||
|
||||
> account []
|
||||
|
||||
> number []
|
||||
|
||||
> exp []
|
||||
|
||||
> sec []
|
||||
|
||||
> street []
|
||||
|
||||
> city []
|
||||
|
||||
"""
|
||||
def add_card(account, number, exp, sec, street, city):
|
||||
|
||||
create("Card", locals())
|
||||
91
src/charge.py
Normal file
91
src/charge.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#from lib import *
|
||||
|
||||
|
||||
|
||||
"""
|
||||
𝑓 charge_card
|
||||
|
||||
Function to charge a credit card.
|
||||
|
||||
> card [Card]
|
||||
|
||||
> order [Order]
|
||||
|
||||
> account [Account]
|
||||
|
||||
"""
|
||||
def charge_card(card, order, account):
|
||||
|
||||
merchantAuth = apicontractsv1.merchantAuthenticationType()
|
||||
|
||||
merchantAuth.name = conf["merchant_username"]
|
||||
|
||||
merchantAuth.transactionKey = conf["merchant_key"]
|
||||
|
||||
creditCard = apicontractsv1.creditCardType()
|
||||
|
||||
creditCard.cardNumber = card.number
|
||||
|
||||
creditCard.expirationDate = card.exp
|
||||
|
||||
creditCard.cardCode = card.sec
|
||||
|
||||
payment = apicontractsv1.paymentType()
|
||||
|
||||
payment.creditCard = creditCard
|
||||
|
||||
orderSV1 = apicontractsv1.paymentType()
|
||||
|
||||
orderSV1.invoiceNumber = order.number
|
||||
|
||||
orderSV1.description = order.desc
|
||||
|
||||
address = apicontractsv1.customerAddressType()
|
||||
|
||||
address.firstName = card.address.firstName
|
||||
|
||||
address.lastName = card.address.lastName
|
||||
|
||||
address.address = card.address.street
|
||||
|
||||
address.city = card.address.city
|
||||
|
||||
address.state = card.address.state
|
||||
|
||||
address.zip = card.address.zipcode
|
||||
|
||||
address.country = card.address.country
|
||||
|
||||
customer = apicontractsv1.customerDataType()
|
||||
|
||||
customer.type = "individual"
|
||||
|
||||
customer.id = account.number
|
||||
|
||||
customer.email = account.email
|
||||
|
||||
request = apicontractsv1.transactionRequestType()
|
||||
|
||||
request.transactionType = "authCaptureTransaction"
|
||||
|
||||
request.amount = order.amount
|
||||
|
||||
request.payment = payment
|
||||
|
||||
request.order = orderSV1
|
||||
|
||||
response.billTo = address
|
||||
|
||||
request.customer = customer
|
||||
|
||||
createRequest = apicontractsv1.createTransactionRequest()
|
||||
|
||||
createRequest.merchantAuthentication = merchantAuth
|
||||
|
||||
createRequest.refId = "MerchantID-0001"
|
||||
|
||||
createRequest.transactionrequest = request
|
||||
|
||||
controller = createtransactioncontroller(createRequest)
|
||||
|
||||
controller.execute()
|
||||
98
src/crud.py
Normal file
98
src/crud.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
𝑓 create
|
||||
|
||||
Create a new row in the database \
|
||||
|
||||
> table [str] Name of the table \
|
||||
|
||||
> args [dict] Key value pairs for the fields \
|
||||
|
||||
⮑ [int] ID of the new row \
|
||||
|
||||
@tool @route
|
||||
|
||||
"""
|
||||
def create(table, args):
|
||||
|
||||
model = models[table]
|
||||
|
||||
query = model.insert(**args)
|
||||
|
||||
row = query.execute()
|
||||
|
||||
instance = select(table, [], ("id", row))[0]
|
||||
|
||||
ipc.emit(table, {
|
||||
|
||||
"method": "create",
|
||||
|
||||
"data": instance
|
||||
|
||||
})
|
||||
|
||||
return row
|
||||
|
||||
|
||||
"""
|
||||
𝑓 update
|
||||
|
||||
Update a row in the database given a diff \
|
||||
|
||||
> table [str] Name of the table in the db \
|
||||
|
||||
> row [int] ID of the row \
|
||||
|
||||
> diff [dict] key value pairs of fields to update \
|
||||
|
||||
"""
|
||||
def update(table, row, diff):
|
||||
|
||||
model = models[table]
|
||||
|
||||
query = model.update(**diff).where(model.id == row)
|
||||
|
||||
query.execute()
|
||||
|
||||
ipc.emit(table, {
|
||||
|
||||
"method": "update",
|
||||
|
||||
"data": {
|
||||
|
||||
"row": row,
|
||||
|
||||
"diff": diff
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
"""
|
||||
𝑓 remove
|
||||
|
||||
Remove a row in the database \
|
||||
|
||||
> table [str] Name of the table in the db \
|
||||
|
||||
> row [int] ID of the row to remove \
|
||||
|
||||
"""
|
||||
def delete(table, row):
|
||||
|
||||
model = models[table]
|
||||
|
||||
query = model.delete().where(model.id == row)
|
||||
|
||||
query.execute()
|
||||
|
||||
ipc.emit(table, {
|
||||
|
||||
"method": "remove",
|
||||
|
||||
"data": row
|
||||
|
||||
})
|
||||
25
src/db.py
Normal file
25
src/db.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
# db = peewee.MySQLDatabase(**conf["db"])
|
||||
|
||||
# db.connect()
|
||||
|
||||
db = None
|
||||
|
||||
|
||||
"""
|
||||
$ Base
|
||||
|
||||
Inherit from Base to define a new model in the
|
||||
|
||||
database. Base is the parent component for all
|
||||
|
||||
database models, as it is connected to db. \
|
||||
|
||||
"""
|
||||
class Base(peewee.Model):
|
||||
|
||||
class Meta:
|
||||
|
||||
database = db
|
||||
128
src/doc.py
Normal file
128
src/doc.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import re
|
||||
|
||||
|
||||
def read_notes(code):
|
||||
|
||||
r = r'"""\n([𝑓𝌏$].*?)"""'
|
||||
|
||||
notes = re.findall(r, code, re.DOTALL)
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
def read_name(note):
|
||||
|
||||
r = r'[𝑓𝌏$]\s(\w+)'
|
||||
|
||||
match = re.search(r, note)
|
||||
|
||||
name = match.group(1)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def read_about(note):
|
||||
|
||||
r = r'\n(.*?)\s\\'
|
||||
|
||||
match = re.search(r, note, re.DOTALL)
|
||||
|
||||
group = match.group(1)
|
||||
|
||||
about = re.sub(r'\n\s{4}', lambda x: "", group)
|
||||
|
||||
return about
|
||||
|
||||
|
||||
def read_params(note):
|
||||
|
||||
r = r'>\s(\w+)\s\[(\w+)\]\s(.*?)\s\\'
|
||||
|
||||
captures = re.findall(r, note, re.DOTALL)
|
||||
|
||||
params = []
|
||||
|
||||
for capture in captures:
|
||||
|
||||
name, form, about = capture
|
||||
|
||||
about = re.sub(r'\n\n\s{4}', lambda x: "", about)
|
||||
|
||||
params.append((name, form, about))
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def read_return(note):
|
||||
|
||||
r = r'⮑\s(\w+)\s\[(\w+)\]\s(.*?)\s\\'
|
||||
|
||||
match = re.search(r, note)
|
||||
|
||||
if match:
|
||||
|
||||
ret = match.group(1, 2, 3)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def read_tags(note):
|
||||
|
||||
r = r'@(\w+)'
|
||||
|
||||
tags = re.findall(r, note)
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def read_template(note):
|
||||
|
||||
r = r'𝌏\s(.*)'
|
||||
|
||||
match = re.search(r, note, re.DOTALL)
|
||||
|
||||
template = match.group(1)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def render_template(template, data):
|
||||
|
||||
r1, r2 = r'(.*?)`(.*?)`', r'\[(\w+)\]'
|
||||
|
||||
match = re.search(r1, template, re.DOTALL)
|
||||
|
||||
res, body = match.groups()
|
||||
|
||||
for data in data:
|
||||
|
||||
chunk = re.sub(r2, lambda x: data[x], body)
|
||||
|
||||
res += chunk
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# import os
|
||||
|
||||
# import pprint
|
||||
|
||||
# for file in ["crud.py"]:
|
||||
|
||||
# with open(file) as f:
|
||||
|
||||
# content = f.read()
|
||||
|
||||
# notes = read_notes(content)
|
||||
|
||||
# for note in notes:
|
||||
|
||||
# name = read_name(note)
|
||||
|
||||
# params = read_params(note)
|
||||
|
||||
# tags = read_tags(note)
|
||||
|
||||
# input(name)
|
||||
|
||||
# input(params)
|
||||
41
src/emit.py
Normal file
41
src/emit.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
class Emitter:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.listeners = {}
|
||||
|
||||
def on(self, channel, cb):
|
||||
|
||||
if channel not in self.listeners:
|
||||
|
||||
self.listeners[channel] = set()
|
||||
|
||||
self.listeners[channel].add(cb)
|
||||
|
||||
def emit(self, channel, *args, **kwargs):
|
||||
|
||||
if channel in self.listeners:
|
||||
|
||||
for cb in self.listeners[channel]:
|
||||
|
||||
cb(*args, **kwargs)
|
||||
|
||||
def remove_listener(self, channel, cb):
|
||||
|
||||
if channel in self.listeners:
|
||||
|
||||
self.listeners[channel].remove(cb)
|
||||
|
||||
|
||||
"""
|
||||
$ ipc
|
||||
|
||||
Event emitter primarly used for db updates and
|
||||
|
||||
watchers \
|
||||
|
||||
"""
|
||||
ipc = Emitter()
|
||||
0
src/err.py
Normal file
0
src/err.py
Normal file
110
src/fields.py
Normal file
110
src/fields.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from lib import *
|
||||
|
||||
"""
|
||||
$ EncryptedField
|
||||
|
||||
Parent class for other db classes that is
|
||||
|
||||
in charge of keeping all the stored values
|
||||
|
||||
encrypted and decrypting them when they
|
||||
|
||||
are queried. \
|
||||
|
||||
"""
|
||||
class EncryptedField(peewee.Field):
|
||||
|
||||
def on_save(self, value):
|
||||
|
||||
return value
|
||||
|
||||
def on_load(self, value):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
"""
|
||||
$ UsernameField
|
||||
|
||||
Username field to ensure that all usernames
|
||||
|
||||
are at least 5 characters long and only have
|
||||
|
||||
letters and numbers. \
|
||||
|
||||
"""
|
||||
class UsernameField(peewee.Field):
|
||||
|
||||
def on_save(self, value):
|
||||
|
||||
return value
|
||||
|
||||
def on_load(self, value):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
"""
|
||||
$ PasswordField
|
||||
|
||||
Field for managing passwords. Passowrds must
|
||||
|
||||
be at least 8 characters long with numbers,
|
||||
|
||||
letters, and symbols. \
|
||||
|
||||
"""
|
||||
class PasswordField(EncryptedField):
|
||||
|
||||
def on_save(self, value):
|
||||
|
||||
return value
|
||||
|
||||
def on_load(self, value):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
"""
|
||||
$ EmailField
|
||||
|
||||
Field for storing emails. There is validation at
|
||||
|
||||
the character level and sending an email
|
||||
|
||||
verification level. \
|
||||
|
||||
"""
|
||||
class EmailField(peewee.Field):
|
||||
|
||||
def on_save(self, value):
|
||||
|
||||
# r = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
|
||||
|
||||
# if not re.match(r, value):
|
||||
|
||||
# raise ValidationError()
|
||||
|
||||
return value
|
||||
|
||||
def on_load(self, value):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
"""
|
||||
$ CharField
|
||||
|
||||
Declaring peewee's CharField here. \
|
||||
|
||||
"""
|
||||
CharField = peewee.CharField
|
||||
|
||||
|
||||
"""
|
||||
$ IntField
|
||||
|
||||
Declaring peewee's IntegerField here. \
|
||||
|
||||
"""
|
||||
IntField = peewee.IntegerField
|
||||
45
src/file.py
Normal file
45
src/file.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
def build_path(path):
|
||||
|
||||
path = path.split("/")
|
||||
|
||||
path = os.path.join(conf["public"], *path)
|
||||
|
||||
if os.path.isdir(path):
|
||||
|
||||
path += "index.html"
|
||||
|
||||
mime = mimetypes.guess_type(path)[0]
|
||||
|
||||
return path, mime
|
||||
|
||||
|
||||
"""
|
||||
𝑓 get_ui_file
|
||||
|
||||
Get a file in the ui folder. Files in the ui
|
||||
|
||||
folder are used by the browser. \
|
||||
|
||||
> path [str] The relative path to the file \
|
||||
|
||||
⮑ [None | dict] \
|
||||
|
||||
"""
|
||||
def get_ui_file(path):
|
||||
|
||||
path, mime = build_path(path)
|
||||
|
||||
if os.path.exists(path):
|
||||
|
||||
with open(path, "rb") as file:
|
||||
|
||||
return {
|
||||
|
||||
"mime": mime,
|
||||
|
||||
"content": file.read()
|
||||
|
||||
}
|
||||
36
src/get.py
Normal file
36
src/get.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
𝑓 select
|
||||
|
||||
Retreive data from the database \
|
||||
|
||||
> table [str] Name of the table to select from \
|
||||
|
||||
> columns [list] The columns to select from \
|
||||
|
||||
> conditions [dict] Key, values to conditions \
|
||||
|
||||
⮑ [list] list of instances \
|
||||
|
||||
@route
|
||||
|
||||
"""
|
||||
def select(table, columns, conditions):
|
||||
|
||||
model = models[table]
|
||||
|
||||
fields = []
|
||||
|
||||
for column in columns:
|
||||
|
||||
fields.append(getattr(model, column))
|
||||
|
||||
query = model.select(*fields)
|
||||
|
||||
for field, value in conditions:
|
||||
|
||||
query.where(getattr(model, field) == value)
|
||||
|
||||
return list(query.dicts())
|
||||
28
src/handle.py
Normal file
28
src/handle.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from lib import *
|
||||
|
||||
"""
|
||||
$ handlers
|
||||
|
||||
Where the various handlers go. One for each
|
||||
|
||||
subdomain. For instance, there is a specific
|
||||
|
||||
handler for websocket requests. \
|
||||
|
||||
"""
|
||||
handlers = {}
|
||||
|
||||
|
||||
def make_handlers():
|
||||
|
||||
for name, note in notes.items():
|
||||
|
||||
for tag in doc.read_tags(note):
|
||||
|
||||
if tag == "handler":
|
||||
|
||||
handler = GET_LIB_REF(name)
|
||||
|
||||
handlers[name] = handler
|
||||
|
||||
print(f"Handlers: {handlers.keys()}")
|
||||
10
src/lib.py
Normal file
10
src/lib.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import doc
|
||||
import sys
|
||||
|
||||
def GET_LIB_REF(name):
|
||||
|
||||
return getattr(sys.modules[__name__], name)
|
||||
|
||||
notes = {}
|
||||
|
||||
__all__ = ["notes", "GET_LIB_REF", "doc"]
|
||||
56
src/link.py
Normal file
56
src/link.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import os
|
||||
import ast
|
||||
import lib
|
||||
import doc
|
||||
import parse
|
||||
import importlib
|
||||
|
||||
queue = os.listdir()
|
||||
|
||||
seen = set()
|
||||
|
||||
while queue:
|
||||
|
||||
file = queue.pop(0)
|
||||
|
||||
if file.endswith(".py"):
|
||||
|
||||
with open(file) as f:
|
||||
|
||||
content = f.read()
|
||||
|
||||
if content.startswith("from lib import *"):
|
||||
|
||||
tree = ast.parse(content)
|
||||
|
||||
checker = parse.DependencyChecker(set(lib.__all__))
|
||||
|
||||
checker.visit(tree)
|
||||
|
||||
if checker.undefined:
|
||||
|
||||
if file in seen:
|
||||
|
||||
print(f"Seen {file}, {checker.undefined}")
|
||||
|
||||
queue.append(file)
|
||||
|
||||
seen.add(file)
|
||||
|
||||
continue
|
||||
|
||||
print(f"Importing {file}")
|
||||
|
||||
module = importlib.import_module(file[:-3])
|
||||
|
||||
for note in doc.read_notes(content):
|
||||
|
||||
name = doc.read_name(note)
|
||||
|
||||
if hasattr(module, name):
|
||||
|
||||
setattr(lib, name, getattr(module, name))
|
||||
|
||||
lib.__all__.append(name)
|
||||
|
||||
lib.notes[name] = note
|
||||
40
src/loop.py
Normal file
40
src/loop.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
runners = {}
|
||||
|
||||
|
||||
def make_runners():
|
||||
|
||||
for name, note in notes.items():
|
||||
|
||||
if "runner" in doc.read_tags(note):
|
||||
|
||||
runner = GET_LIB_REF(name)
|
||||
|
||||
runners[name] = runner
|
||||
|
||||
print(f"Runners: {runners.keys()}")
|
||||
|
||||
|
||||
def handle_exception(task):
|
||||
|
||||
print(f"Task done: {task.result}")
|
||||
|
||||
|
||||
async def start_runners():
|
||||
|
||||
for name, runner in runners.items():
|
||||
|
||||
task = asyncio.create_task(runner())
|
||||
|
||||
task.add_done_callback(handle_exception)
|
||||
|
||||
while True:
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
def run():
|
||||
|
||||
asyncio.run(start_runners())
|
||||
19
src/main.py
Normal file
19
src/main.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import link
|
||||
|
||||
from api import make_routes
|
||||
|
||||
from models import make_models
|
||||
|
||||
from handle import make_handlers
|
||||
|
||||
from loop import run, make_runners
|
||||
|
||||
make_routes()
|
||||
|
||||
# make_models()
|
||||
|
||||
make_handlers()
|
||||
|
||||
make_runners()
|
||||
|
||||
run()
|
||||
42
src/models.py
Normal file
42
src/models.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
$ models
|
||||
|
||||
The models of the database. Each model represents
|
||||
|
||||
a table and is the interface to work with to
|
||||
|
||||
communicate with the db. \
|
||||
|
||||
"""
|
||||
models = {}
|
||||
|
||||
|
||||
def create_model(table, columns):
|
||||
|
||||
fields = {}
|
||||
|
||||
for name, valid, about in columns:
|
||||
|
||||
fields[name] = GET_LIB_REF(valid)
|
||||
|
||||
return type(table, (Base,), fields)
|
||||
|
||||
|
||||
def make_models():
|
||||
|
||||
for name, note in notes.items():
|
||||
|
||||
if note.startswith("𝌏"):
|
||||
|
||||
columns = doc.read_params(note)
|
||||
|
||||
model = create_model(name, columns)
|
||||
|
||||
model.create_table()
|
||||
|
||||
models[name] = model
|
||||
|
||||
print(f"Models: {models.keys()}")
|
||||
106
src/order.py
Normal file
106
src/order.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
𝌏 Order
|
||||
|
||||
A customer order. Very basic for now, just takes in an
|
||||
|
||||
email and a product. The product is the ID of a
|
||||
|
||||
registered product in the database \
|
||||
|
||||
`
|
||||
<
|
||||
|
||||
< [number] />
|
||||
|
||||
< [product.name] />
|
||||
|
||||
< [description] />
|
||||
|
||||
< [format(timestamp)] />
|
||||
|
||||
/>
|
||||
|
||||
`
|
||||
|
||||
> number [UidField] A unique random number \
|
||||
|
||||
> email [str] Email for order fulfillment and tracking \
|
||||
|
||||
> decription [CharField] Brief description or justification \
|
||||
|
||||
> product [Product] Product ID from the Products model \
|
||||
|
||||
> timestamp [FloatField] time.time of order placement \
|
||||
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
𝑓 place_order
|
||||
|
||||
Place an order for a product. Creates an order in the
|
||||
|
||||
database so that the billing runner will handle it \
|
||||
|
||||
> email [str] An email to connect to the order \
|
||||
|
||||
> product [int] A product from the Products table \
|
||||
|
||||
"""
|
||||
def place_order(email, product):
|
||||
|
||||
number = uuid.uuid4()
|
||||
|
||||
timestamp = time.time()
|
||||
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
|
||||
formatted = dt.strftime("%b %d, %Y at %-I:%M %p")
|
||||
|
||||
desc = f"Order {number} placed on {formatted}"
|
||||
|
||||
create("Order", {
|
||||
|
||||
"email": email,
|
||||
|
||||
"product": product,
|
||||
|
||||
"number": number,
|
||||
|
||||
"timestamp": timestamp,
|
||||
|
||||
"description": desc
|
||||
|
||||
})
|
||||
|
||||
|
||||
"""
|
||||
𝑓 bill
|
||||
|
||||
Manage outstanding orders by attempting to
|
||||
|
||||
collect payments on time (referring to the
|
||||
|
||||
payment plans), also handling any payment
|
||||
|
||||
errors. \
|
||||
|
||||
@runner
|
||||
|
||||
"""
|
||||
async def bill():
|
||||
|
||||
while True:
|
||||
|
||||
orders = select("Orders", [], [])
|
||||
|
||||
for order in orders:
|
||||
|
||||
if order["paid"]:
|
||||
|
||||
continue
|
||||
|
||||
product = select("Product", [], ("id", order["product"]))
|
||||
196
src/parse.py
Normal file
196
src/parse.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import ast
|
||||
import builtins
|
||||
|
||||
|
||||
class DependencyChecker(ast.NodeVisitor):
|
||||
|
||||
def __init__(self, env):
|
||||
|
||||
self.undefined = []
|
||||
|
||||
self.scopes = [set(dir(builtins))]
|
||||
|
||||
self.scopes.append(env)
|
||||
|
||||
|
||||
def visit_Import(self, node):
|
||||
|
||||
curr_scope = self.scopes[-1]
|
||||
|
||||
for name in node.names:
|
||||
|
||||
curr_scope.add(name.name)
|
||||
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
|
||||
curr_scope = self.scopes[-1]
|
||||
|
||||
for name in node.names:
|
||||
|
||||
if name.name == "*":
|
||||
|
||||
continue
|
||||
|
||||
curr_scope.add(name.name)
|
||||
|
||||
|
||||
def visit_Assign(self, node):
|
||||
|
||||
curr_scope = self.scopes[-1]
|
||||
|
||||
for target in node.targets:
|
||||
|
||||
if isinstance(target, ast.Name):
|
||||
|
||||
curr_scope.add(target.id)
|
||||
|
||||
elif isinstance(target, ast.Tuple):
|
||||
|
||||
for name in target.elts:
|
||||
|
||||
curr_scope.add(name.id)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def visit_For(self, node):
|
||||
|
||||
new_scope = set()
|
||||
|
||||
if isinstance(node.target, ast.Name):
|
||||
|
||||
new_scope.add(node.target.id)
|
||||
|
||||
elif isinstance(node.target, ast.Tuple):
|
||||
|
||||
for name in node.target.elts:
|
||||
|
||||
new_scope.add(name.id)
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_With(self, node):
|
||||
|
||||
new_scope = set()
|
||||
|
||||
for item in node.items:
|
||||
|
||||
name = item.optional_vars
|
||||
|
||||
new_scope.add(name.id)
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_While(self, node):
|
||||
|
||||
new_scope = set()
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_If(self, node):
|
||||
|
||||
new_scope = set()
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
|
||||
curr_scope = self.scopes[-1]
|
||||
|
||||
curr_scope.add(node.name)
|
||||
|
||||
new_scope = set()
|
||||
|
||||
for arg in node.args.args:
|
||||
|
||||
new_scope.add(arg.arg)
|
||||
|
||||
if node.args.vararg:
|
||||
|
||||
new_scope.add(node.args.vararg.arg)
|
||||
|
||||
if node.args.kwarg:
|
||||
|
||||
new_scope.add(node.args.kwarg.arg)
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
|
||||
self.visit_FunctionDef(node)
|
||||
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
|
||||
curr_scope = self.scopes[-1]
|
||||
|
||||
curr_scope.add(node.name)
|
||||
|
||||
new_scope = set()
|
||||
|
||||
self.scopes.append(new_scope)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
|
||||
def visit_Name(self, node):
|
||||
|
||||
for scope in self.scopes:
|
||||
|
||||
if node.id in scope:
|
||||
|
||||
return
|
||||
|
||||
self.undefined.append(node.id)
|
||||
|
||||
|
||||
|
||||
# import os
|
||||
|
||||
# import ast
|
||||
|
||||
# for file in ["api.py"]:
|
||||
|
||||
# if file.endswith(".py"):
|
||||
|
||||
# print(file)
|
||||
|
||||
# with open(file) as f:
|
||||
|
||||
# content = f.read()
|
||||
|
||||
# tree = ast.parse(content)
|
||||
|
||||
# checker = DependencyChecker(set())
|
||||
|
||||
# checker.visit(tree)
|
||||
|
||||
# input(checker.undefined)
|
||||
32
src/product.py
Normal file
32
src/product.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
𝌏 Product
|
||||
|
||||
Something that Crimata is selling. A product represents
|
||||
|
||||
the thing the customer will get in return for their
|
||||
|
||||
purchase. \
|
||||
|
||||
`
|
||||
<
|
||||
|
||||
<name>
|
||||
|
||||
<photo>
|
||||
|
||||
<description>
|
||||
|
||||
>
|
||||
|
||||
`
|
||||
|
||||
> name [CharField] Name of the product \
|
||||
|
||||
> photo [PhotoField] Preview proto of the product \
|
||||
|
||||
> description [CharField] Brief about \
|
||||
|
||||
"""
|
||||
99
src/route.py
Normal file
99
src/route.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
def step(path):
|
||||
|
||||
for part in path.strip("/").split("/"):
|
||||
|
||||
yield part
|
||||
|
||||
|
||||
"""
|
||||
𝑓 api
|
||||
|
||||
Handles all http(s) requests by parsing the path
|
||||
|
||||
to find the route, calling the route, and
|
||||
|
||||
wrapping the return in a response object. \
|
||||
|
||||
> request [web.Request] aiohttp Request \
|
||||
|
||||
⮑ [web.Response] aiohttp Response object \
|
||||
|
||||
@handler
|
||||
|
||||
"""
|
||||
async def api(request):
|
||||
|
||||
path = step(request.path)
|
||||
|
||||
func = routes[next(path)]
|
||||
|
||||
args = await request.json()
|
||||
|
||||
res = web.Response(headers=conf["cors"])
|
||||
|
||||
res.body = func(args)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
"""
|
||||
𝑓 ws
|
||||
|
||||
Handles all websocket requests. Upgrades the request
|
||||
|
||||
to the websocket protocol and then calls the
|
||||
|
||||
handler. \
|
||||
|
||||
> request [web.Request] aiohttp Request object
|
||||
|
||||
@handler
|
||||
|
||||
"""
|
||||
async def ws(request):
|
||||
|
||||
path = step(request.path)
|
||||
|
||||
func = routes[next(path)]
|
||||
|
||||
ws = web.WebSocketResponse(protocols=("chat"))
|
||||
|
||||
await ws.prepare(request)
|
||||
|
||||
args = await ws.receive_json()
|
||||
|
||||
await func(ws, **args)
|
||||
|
||||
|
||||
"""
|
||||
𝑓 _
|
||||
|
||||
Handles all http(s) file requests, like when the
|
||||
|
||||
browser requests the html for the UI. \
|
||||
|
||||
> request [web.Request] aiohttp request object \
|
||||
|
||||
⮑ [web.Response] aiohttp response object \
|
||||
|
||||
@handler
|
||||
|
||||
"""
|
||||
async def _(request):
|
||||
|
||||
file = get_ui_file(request.path)
|
||||
|
||||
if not file:
|
||||
|
||||
return web.Response(status=404)
|
||||
|
||||
res = web.Response()
|
||||
|
||||
res.body = file["content"]
|
||||
|
||||
res.headers["Content-Type"] = file["mime"]
|
||||
|
||||
return res
|
||||
60
src/serve.py
Normal file
60
src/serve.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
def get_subdomain(host):
|
||||
|
||||
res = "_"
|
||||
|
||||
parts = host.split(".")
|
||||
|
||||
if len(parts) >= 2:
|
||||
|
||||
res = parts[0]
|
||||
|
||||
return res
|
||||
|
||||
|
||||
async def handler(request):
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
|
||||
return web.Response(headers=conf["cors"])
|
||||
|
||||
sub = get_subdomain(request.host)
|
||||
|
||||
func = handlers[sub]
|
||||
|
||||
res = await func(request)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
"""
|
||||
𝑓 lift
|
||||
|
||||
Handles all incoming web requests. \
|
||||
|
||||
> port [int] The port to listen on \
|
||||
|
||||
@runner
|
||||
|
||||
"""
|
||||
async def lift():
|
||||
|
||||
port = conf["port"]
|
||||
|
||||
server = web.Server(handler)
|
||||
|
||||
runner = web.ServerRunner(server)
|
||||
|
||||
await runner.setup()
|
||||
|
||||
site = web.TCPSite(runner, None, port)
|
||||
|
||||
await site.start()
|
||||
|
||||
print(f"Running on port {port}!")
|
||||
|
||||
while True:
|
||||
|
||||
await asyncio.sleep(1)
|
||||
20
src/ui/index.html
Normal file
20
src/ui/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="./main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="./req.js"></script>
|
||||
<script src="./watch.js"></script>
|
||||
|
||||
<!-- vue@3.4.21 -->
|
||||
<script src="./vue.js"></script>
|
||||
|
||||
<script type="module" src="./index.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
10
src/ui/index.js
Normal file
10
src/ui/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Main from "./main.js";
|
||||
|
||||
|
||||
console.log(Vue.version);
|
||||
|
||||
|
||||
const app = Vue.createApp(Main);
|
||||
|
||||
|
||||
app.mount("#root");
|
||||
0
src/ui/main.css
Normal file
0
src/ui/main.css
Normal file
57
src/ui/main.js
Normal file
57
src/ui/main.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Account from "./account.js";
|
||||
|
||||
const Main = {
|
||||
|
||||
components: {
|
||||
|
||||
Account
|
||||
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
const accounts = watch("Account");
|
||||
|
||||
const cards = watch("Card");
|
||||
|
||||
const orders = watch("Order");
|
||||
|
||||
const products = watch("Products");
|
||||
|
||||
reqBase({
|
||||
|
||||
path: "/create_account",
|
||||
|
||||
args: {
|
||||
|
||||
username: "gundy97",
|
||||
|
||||
email: "adgundersen@gmail.com"
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return { accounts, cards, orders, products };
|
||||
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="main">
|
||||
|
||||
<Account v-for="account in accounts" v-bind="account" />
|
||||
|
||||
<Card v-for="card in cards" v-bind="card" />
|
||||
|
||||
<Orders v-for="order in orders" v-bind="order" />
|
||||
|
||||
<Product v-for="product in products" v-bind="product" />
|
||||
|
||||
</div>
|
||||
`
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default Main;
|
||||
43
src/ui/req.js
Normal file
43
src/ui/req.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const reqBase = async ({ path, args }) => {
|
||||
|
||||
const url = buildRequestURL("api", path);
|
||||
|
||||
let res = await fetch(url, {
|
||||
|
||||
method: "post",
|
||||
|
||||
body: JSON.stringify(args)
|
||||
|
||||
});
|
||||
|
||||
return res
|
||||
|
||||
};
|
||||
|
||||
|
||||
const wsBase = ({ path, message, cb }) => {
|
||||
|
||||
const url = buildRequestURL("ws", path);
|
||||
|
||||
const ws = new WebSocket(url, ["chat", window.session]);
|
||||
|
||||
ws.onopen = (e) => {
|
||||
|
||||
ws.send(JSON.stringify(message));
|
||||
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
|
||||
cb(JSON.parse(e.data));
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
const buildRequestURL = (sub, path) => {
|
||||
|
||||
return `http://${sub}.localhost:8080${path}`;
|
||||
|
||||
};
|
||||
16669
src/ui/vue.js
Normal file
16669
src/ui/vue.js
Normal file
File diff suppressed because it is too large
Load diff
63
src/ui/watch.js
Normal file
63
src/ui/watch.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const watch = (table) => {
|
||||
|
||||
ref = Vue.ref([]);
|
||||
|
||||
wsBase({
|
||||
|
||||
path: "/watch",
|
||||
|
||||
message: {
|
||||
|
||||
"table": table
|
||||
|
||||
},
|
||||
|
||||
cb: (method, data) => {
|
||||
|
||||
if (method == "set") {
|
||||
|
||||
ref.value = data;
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (method == "create") {
|
||||
|
||||
ref.value.push(data);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (method == "update") {
|
||||
|
||||
const instance = ref.value.find(
|
||||
|
||||
instance => {
|
||||
|
||||
return instance.id == data.id;
|
||||
|
||||
});
|
||||
|
||||
Object.assign(instance, data.diff);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (method == "remove") {
|
||||
|
||||
ref.value.remove(data);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return ref;
|
||||
|
||||
};
|
||||
54
src/users.py
Normal file
54
src/users.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
"""
|
||||
𝌏 Account
|
||||
|
||||
A user's account with basic profile info \
|
||||
|
||||
`
|
||||
<
|
||||
|
||||
<img .avatar [photo] />
|
||||
|
||||
< .info
|
||||
|
||||
< [name] />
|
||||
|
||||
< [email] />
|
||||
|
||||
/>
|
||||
|
||||
/>
|
||||
|
||||
`
|
||||
|
||||
> username [UsernameField] Typical username behavior \
|
||||
|
||||
> email [EmailField] Ideally verified \
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
𝑓 create_account
|
||||
|
||||
Create a new account in the system. Takes in basic
|
||||
|
||||
profile info like username, password, etc.
|
||||
|
||||
> username [str]
|
||||
|
||||
> email [str]
|
||||
|
||||
⮑ [int] the unique account number
|
||||
|
||||
@route
|
||||
|
||||
"""
|
||||
async def create_account(username, email):
|
||||
|
||||
number = uuid.uuid4()
|
||||
|
||||
create("Account", locals())
|
||||
|
||||
return number
|
||||
20
src/util.py
Normal file
20
src/util.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from lib import *
|
||||
|
||||
|
||||
def load_file(path, options="r"):
|
||||
|
||||
with open(path, options) as file:
|
||||
|
||||
return file.read()
|
||||
|
||||
|
||||
def load_yaml(path):
|
||||
|
||||
return yaml.safe_load(load_file(path))
|
||||
|
||||
|
||||
"""
|
||||
𝑓 conf
|
||||
|
||||
"""
|
||||
conf = load_yaml("../conf.yml")
|
||||
56
src/vendor.py
Normal file
56
src/vendor.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from lib import *
|
||||
|
||||
"""
|
||||
$ os
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
"""
|
||||
$ time
|
||||
|
||||
"""
|
||||
import time
|
||||
|
||||
"""
|
||||
$ datetime
|
||||
|
||||
"""
|
||||
import datetime
|
||||
|
||||
"""
|
||||
$ yaml
|
||||
|
||||
"""
|
||||
import yaml
|
||||
|
||||
"""
|
||||
$ peewee
|
||||
|
||||
"""
|
||||
import peewee
|
||||
|
||||
"""
|
||||
$ asyncio
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
"""
|
||||
$ mimetypes
|
||||
|
||||
"""
|
||||
import mimetypes
|
||||
|
||||
"""
|
||||
$ web
|
||||
|
||||
"""
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
"""
|
||||
$ uuid
|
||||
|
||||
"""
|
||||
import uuid
|
||||
29
src/watch.py
Normal file
29
src/watch.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from lib import *
|
||||
|
||||
"""
|
||||
𝑓 watch
|
||||
|
||||
Get live updates on a table in the db \
|
||||
|
||||
> table [str] the table in the db to track \
|
||||
|
||||
@route
|
||||
|
||||
"""
|
||||
async def watch(conn, table):
|
||||
|
||||
def cb(method, data):
|
||||
|
||||
asyncio.create_task(conn.send_json(locals()))
|
||||
|
||||
instances = select(table, [], [])
|
||||
|
||||
cb("set", instances)
|
||||
|
||||
ipc.on(table, cb)
|
||||
|
||||
while not conn.closed:
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
ipc.remove_listener(table, cb)
|
||||
Loading…
Reference in a new issue