init commit

This commit is contained in:
Andrew Gundersen 2025-08-11 14:51:24 -04:00
commit 439c894b39
37 changed files with 18468 additions and 0 deletions

14
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

110
src/fields.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

57
src/ui/main.js Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

63
src/ui/watch.js Normal file
View 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
View 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
View 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
View 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
View 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)