Use Python as a declarative Markup language with field dependencies automatically resolved.
⚠️ This is a simple experimental project, not intended for production configuration management.
- 📝 Write structured data in Python with nested dictionaries and objects.
- 🔄 Support multiple file merging and automatically updates dependent fields when other fields change.
- 📦 Supports python operations like importing libraries and calling functions during configuring .
pip install git+https://github.com/liyif/Tomori.gitconfig.tomori
from tomori import opendata
# Helper functions
def make_url(service, host, port):
return f"http://{host}:{port}/{service}"
with opendata() as d:
# ⚠️ Important note:
# In `opendata()` blocks, statements except Assign and With are not allowed.
# Application info
with d.app as app:
# Different from standard python,
# the as-name there has a strict scope in the block.
app.name = "MyApp"
app.env = "dev"
app.host = "localhost"
app.port = 8080
# or
# d.app = dict(name="MyApp",env="dev",host="localhost",port=8080)
d.app.debug = (d.app.env == "dev")
# Services
with d.services as s:
with s.auth as auth:
auth.name = "auth"
auth.url = make_url(auth.name, d.app.host, d.app.port)
with s.payments as payments:
payments.name = "payments"
payments.url = make_url(payments.name, d.app.host, d.app.port)
with s.notifications as notif:
notif.name = "notifications"
notif.url = make_url(notif.name, d.app.host, d.app.port)
# Derived fields
d.serviceCount = len(d.services)
d.dynamicFlag = not d.app.debug
config.prod.tomori
from tomori import opendata, values
with opendata() as d:
d.app.env = "prod"
d.app.host = "0.0.0.0"
# ⚠️ Important note:
# Each field can be redeclared or override.
# However, please avoid patterns where a field is read and reassigned in the same expression (self-modification).
# Not Recommend:
# d.app = d.app | { "port": 80 }
# No guarantee that is evaluated after all subfields are built.
# Just this:
d.app.port = 80
# ⚠️ Important note:
# Using `d.services.values()` would create a dependency on the `values` attribute itself,
# which might be evaluated before all other subfields (auth, payments, notifications) are built.
# Using `tomori.values(d.services)` or simply `getattr(d.services, "values")()` ensures
# that the dependency is on `d.services` itself,
# so all fields are fully constructed before the comprehension executes.
d.servicesUrls = [ s.url for s in values(d.services) ]
main.py
from tomori import Data
import json
data = (Data()
.include_file("config.tomori")
.include_file("config.prod.tomori")
.resolve())
print(json.dumps(data, indent=4))output
{
"app": {
"name": "MyApp",
"env": "prod",
"host": "0.0.0.0",
"port": 80,
"debug": false
},
"services": {
"auth": {
"name": "auth",
"url": "http://0.0.0.0:80/auth"
},
"payments": {
"name": "payments",
"url": "http://0.0.0.0:80/payments"
},
"notifications": {
"name": "notifications",
"url": "http://0.0.0.0:80/notifications"
}
},
"dynamicFlag": true,
"serviceCount": 3,
"servicesUrls": [
"http://0.0.0.0:80/auth",
"http://0.0.0.0:80/payments",
"http://0.0.0.0:80/notifications"
]
}Tomori uses libcst to do static analysis on inputs, then toposort the statements by their reads and writes.
So you may not expect it can magically resolve any dependencies like a reactive spreadsheet.