Compare commits

...

20 Commits

Author SHA1 Message Date
Hazel Noack
9040b26279 implemented option to disable requests 2025-07-30 12:16:34 +02:00
Hazel Noack
98d656b2fe validate type of vars from config file 2025-07-30 12:00:32 +02:00
Hazel Noack
abadff31d8 imporved logging 2025-07-30 11:55:33 +02:00
Hazel Noack
ba36851336 changed the returncode to make sure it detects python 2025-07-30 11:16:03 +02:00
Hazel Noack
d9e6bac410 rewrote resolving the symlinks in comprehension 2025-07-30 11:08:27 +02:00
Hazel Noack
69f6a11874 feat: implemented error handling for empty lists 2025-07-30 11:00:18 +02:00
amnesia
8e8409afc4 typo 2025-07-29 21:13:44 +02:00
fd51a0625f updated responses json 2025-07-29 20:38:12 +02:00
2162cf78cd replaced response json 2025-07-29 20:35:22 +02:00
8036dc33b3 fixed symlinc resolving 2025-07-29 20:32:16 +02:00
Hazel Noack
d968795628 cli script to recompile config 2025-07-29 12:46:56 +02:00
Hazel Noack
05f7003ea8 removed development cli script 2025-07-29 12:38:08 +02:00
3a7b46219e Merge pull request 'building_config' (#1) from building_config into main
Reviewed-on: #1
2025-07-29 10:28:01 +00:00
Hazel Noack
ad1aff0438 completely disable bad logger 2025-07-29 12:24:47 +02:00
Hazel Noack
3213e8a21f added option for local compile 2025-07-29 12:22:41 +02:00
Hazel Noack
94c585fa7a added verbose option 2025-07-29 12:19:06 +02:00
Hazel Noack
a8a1f425cc ensure running in venv 2025-07-29 12:08:27 +02:00
Hazel Noack
be95c352d6 cleaned up compiled config file 2025-07-29 11:56:39 +02:00
Hazel Noack
d1fe9d55d5 Merge branch 'main' into building_config 2025-07-29 11:33:21 +02:00
Hazel Noack
ac7360ffe8 added logging 2025-07-29 11:31:49 +02:00
7 changed files with 308 additions and 81 deletions

View File

@@ -1,10 +1,10 @@
#!/home/fname/Projects/OpenSource/python-mommy-venv/.venv/bin/python3
#!.venv/bin/python3
import requests
from pathlib import Path
import json
CARGO_MOMMY_DATA = "https://raw.githubusercontent.com/diamondburned/go-mommy/refs/heads/main/responses.json"
CARGO_MOMMY_DATA = "https://raw.githubusercontent.com/Gankra/cargo-mommy/refs/heads/main/responses.json"
MODULE_PATH = Path("python_mommy_venv")
@@ -14,7 +14,7 @@ if __name__ == "__main__":
res = requests.get(CARGO_MOMMY_DATA)
if not res.ok:
raise Exception(f"couldn't fetch {CARGO_MOMMY_DATA} ({res.status_code})")
print(f"writing {Path(MODULE_PATH, 'responses.json')}")
with Path(MODULE_PATH, "responses.json").open("w") as f:
json.dump(res.json(), f, indent=4)

View File

@@ -14,7 +14,7 @@ version = "0.0.0"
license-files = ["LICENSE"]
[project.scripts]
python-mommy-dev = "python_mommy_venv.__main__:development"
mommify-venv-compile = "python_mommy_venv.__main__:cli_compile_config"
mommify-venv = "python_mommy_venv.__main__:mommify_venv"
[build-system]

View File

@@ -1,11 +1,9 @@
import random
import subprocess
import sys
from typing import Optional
import json
from .responses import COMPILED_CONFIG_FILE
from .static import colors
from .static import colors, get_compiled_config_file
def get_response_from_situation(situation: str, colorize: Optional[bool] = None):
@@ -13,7 +11,7 @@ def get_response_from_situation(situation: str, colorize: Optional[bool] = None)
colorize = sys.stdout.isatty()
# get message
config = json.loads(COMPILED_CONFIG_FILE.read_text())
config = json.loads(get_compiled_config_file().read_text())
existing_moods = list(config["moods"].keys())
template_options = config["moods"][random.choice(existing_moods)][situation]
template: str = random.choice(template_options)

View File

@@ -1,19 +1,38 @@
import sys
from pathlib import Path
import stat
import subprocess
import logging
import json
import argparse
import toml
from . import get_response
from .responses import compile_config
from .static import IS_VENV, VENV_DIRECTORY, CONFIG_DIRECTORY, COMPILED_CONFIG_FILE_NAME
from ntpath import devnull
def development():
s = "positive"
if len(sys.argv) > 1:
s = sys.argv[1]
logging.basicConfig(
format='%(message)s',
force=True,
)
log_level = logging.INFO
mommy_logger = logging.getLogger("mommy")
mommy_logger.setLevel(logging.INFO)
serious_logger = logging.getLogger("serious")
serious_logger.setLevel(50)
def config_logging(verbose: bool):
if verbose:
logging.basicConfig(
format=logging.BASIC_FORMAT,
force=True,
)
logging.getLogger().setLevel(logging.DEBUG)
mommy_logger.setLevel(50)
serious_logger.setLevel(logging.DEBUG)
compile_config()
WRAPPER_TEMPLATE = """#!{inner_bin}
# -*- coding: utf-8 -*-
@@ -56,8 +75,8 @@ PIP_HOOK = """# GENERATED BY MOMMY
first_line = text.split("\\n")[0]
if not ("inner_" in first_line and first_line.startswith("#!")):
continue
continue
print(f"mommifying " + str(path))
text = text.replace("inner_", "", 1)
@@ -67,63 +86,167 @@ PIP_HOOK = """# GENERATED BY MOMMY
sys.exit(code)"""
def mommify_pip(path: Path):
def assert_venv(only_warn: bool = False):
if not IS_VENV:
mommy_logger.error("mommy doesn't run in a virtual environment~")
serious_logger.error("this should run in a virtual environment")
if not only_warn:
exit(1)
def write_compile_config(local: bool, disable_requests: bool = False):
assert_venv(only_warn=not local)
compiled_base_dir = VENV_DIRECTORY if local else CONFIG_DIRECTORY
compiled_config_file = compiled_base_dir / COMPILED_CONFIG_FILE_NAME
compiled = compile_config(disable_requests=disable_requests)
mommy_logger.info("mommy writes its moods in %s", compiled_config_file)
serious_logger.info("writing compiled config file to %s", compiled_config_file)
compiled_base_dir.mkdir(parents=True, exist_ok=True)
with compiled_config_file.open("w") as f:
json.dump(compiled, f, indent=4)
if not local:
(VENV_DIRECTORY / COMPILED_CONFIG_FILE_NAME).unlink(missing_ok=True)
def wrap_interpreter(path: Path, symlink_target: Path):
mommy_logger.info("mommy found a symlink to an interpreter~ %s", str(path))
serious_logger.info("interpreter symlink found at %s", str(path))
inner_symlink = path.parent / ("inner_" + path.name)
if inner_symlink.exists():
raise Exception("inner symlink somehow already exists. This shouldn't happen because of prior checks")
mommy_logger.info("mommy shows her girl where the interpreter is: %s -> %s", inner_symlink, symlink_target)
serious_logger.info("creating symlink: %s -> %s", inner_symlink, symlink_target)
inner_symlink.symlink_to(symlink_target)
# remove original symlink
mommy_logger.info("mommy deletes the original interpreter~ %s", path)
serious_logger.info("deleting original symlink %s", path)
path.unlink()
# creating the wrapper string
mommy_logger.info("mommy writes wrapper script as %s", Path)
serious_logger.info("writing wrapper script at %s", path)
with path.open("w") as f:
f.write(WRAPPER_TEMPLATE.format(inner_bin=str(inner_symlink)))
serious_logger.info("making wrapper script executable")
path.chmod(path.stat().st_mode | stat.S_IEXEC)
def install_pip_hook(path: Path):
text: str
with path.open("r") as f:
text = f.read()
if "# GENERATED BY MOMMY" in text:
print(f"pip hook already installed in {path}")
mommy_logger.info("ahhhhh mommy already watches %s", str(path))
serious_logger.info("pip hook already installed at %s", str(path))
return
print(f"installing pip hook in {path}")
mommy_logger.info("mommy needs to keep an eye on this little pip~ %s", str(path))
serious_logger.info("installing pip hook at %s", str(path))
text = text.replace("sys.exit(main())", PIP_HOOK, 1)
with path.open("w") as f:
f.write(text)
def cli_compile_config():
parser = argparse.ArgumentParser(description="only recompile the config")
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="enable verbose and serious output"
)
parser.add_argument(
"-l", "--local",
action="store_true",
help="compile the config only for the current virtual environment"
)
parser.add_argument(
"-r", "--no-requests",
action="store_true",
help="by default if makes one request to GitHub to fetch the newest responses, this disables that"
)
args = parser.parse_args()
config_logging(args.verbose)
write_compile_config(args.local, disable_requests=args.no_requests)
def mommify_venv():
compile_config()
parser = argparse.ArgumentParser(description="patch the virtual environment to use mommy")
v = ".venv"
if len(sys.argv) > 1:
v = sys.argv[1]
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="enable verbose and serious output"
)
bin_path = Path(v, "bin")
parser.add_argument(
"-l", "--local",
action="store_true",
help="compile the config only for the current virtual environment"
)
parser.add_argument(
"-r", "--no-requests",
action="store_true",
help="by default if makes one request to GitHub to fetch the newest responses, this disables that"
)
args = parser.parse_args()
config_logging(args.verbose)
assert_venv()
write_compile_config(args.local)
mommy_logger.info("")
bin_path = VENV_DIRECTORY / "bin"
bin_path = bin_path.resolve()
print(bin_path)
for path in bin_path.iterdir():
if not path.is_symlink():
if path.name.startswith("pip"):
mommify_pip(path)
continue
mommy_logger.info("mommy looks in %s to mess your system up~ <33", str(bin_path))
serious_logger.info("scanning binary directory of venv at %s", str(bin_path))
# resolving the symlinks before making edits to anything because else it will mess up the resolving
# and link to the wrapper instead of the original script
resolved_symlinks = {
path.name: path.resolve()
for path in bin_path.iterdir()
if path.is_symlink()
}
serious_logger.debug("resolved symlinks:\n%s", "\n".join(
f"\t{name} => {str(target)}"
for name, target in resolved_symlinks.items()
))
for path in list(bin_path.iterdir()):
name = path.name
if name.startswith("inner_"):
continue
target = path.resolve()
print("")
print(f"modifying {name} ({target})")
if path.is_symlink():
# could be python interpreter
# check for both just to be more expressive
if name.startswith("inner_"):
continue
RANDOM_RETURNCODE = 161
if subprocess.run([str(path), '-c', f'exit({RANDOM_RETURNCODE})']).returncode != RANDOM_RETURNCODE:
continue
# creating inner symlink
inner_bin = Path(bin_path, "inner_" + name)
if inner_bin.exists():
print(f"inner symlink does already exist {inner_bin}")
print("skipping")
continue
wrap_interpreter(path, resolved_symlinks[path.name])
print(f"creating symlink: {inner_bin} -> {target}")
Path(bin_path, "inner_" + name).symlink_to(target)
else:
# could be pip
if not name.startswith("pip"):
continue
# remove original symlink
print(f"removing original symlink: {path}")
path.unlink()
# creating the wrapper string
print("writing wrapper script")
with path.open("w") as f:
f.write(WRAPPER_TEMPLATE.format(inner_bin=str(inner_bin)))
print("making wrapper script executable")
path.chmod(path.stat().st_mode | stat.S_IEXEC)
install_pip_hook(path)

View File

@@ -17,6 +17,8 @@
"well done~!\n{role} is so happy for you~",
"what a good {affectionate_term} you are~",
"that's {role}'s clever little {affectionate_term}~",
"you're doing so well~!",
"you're making {role} so happy~",
"{role} loves {pronoun} cute little {affectionate_term}~"
],
"negative": [
@@ -24,6 +26,7 @@
"don't forget to hydrate~",
"aww, you'll get it next time~",
"do you need {role}'s help~?",
"everything's gonna be ok~",
"{role} still loves you no matter what~",
"oh no did {role}'s little {affectionate_term} make a big mess~?",
"{role} knows {pronoun} little {affectionate_term} can do better~",
@@ -33,7 +36,26 @@
"oh, darling, you're almost there~",
"does {role}'s little {affectionate_term} need a bit of a break~?",
"oops~! {role} loves you anyways~",
"try again for {role}, {affectionate_term}~",
"don't worry, {role} knows you can do it~"
],
"overflow": [
"{role} has executed too many times and needs to take a nap~"
]
},
"ominous": {
"positive": [
"What you have set in motion today will be remembered for aeons to come!",
"{role} will see to it that {pronoun} little {affectionate_term}'s name is feared~",
"{role} is proud of the evil seed {pronoun} {affectionate_term} has planted into this accursed world"
],
"negative": [
"Ah, failure? {role} will make sure the stars are right next time",
"Does {role}'s little {affectionate_term} need more time for worship~?",
"May the mark of the beast stain your flesh forever, {role} will haunt your soul forevermore"
],
"overflow": [
"THOU HAST DRUNK TOO DEEPLY OF THE FONT"
]
},
"thirsty": {
@@ -46,10 +68,11 @@
"*pats your butt*\nthat's a good {affectionate_term}~",
"*drags {pronoun} nail along your cheek*\nsuch a good {affectionate_term}~",
"*bites {pronoun} lip*\nmhmm~",
"give {pronoun} a kiss~",
"give {role} a kiss~",
"*heavy breathing against your neck*"
],
"negative": [
"you're so cute when you're flustered~",
"do you think you're going to get a reward from {role} like that~?",
"*grabs your hair and pulls your head back*\nyou can do better than that for {role} can't you~?",
"if you don't learn how to code better, {role} is going to put you in time-out~",
@@ -58,6 +81,9 @@
"gosh you must be flustered~",
"are you just keysmashing now~?\ncute~",
"is {role}'s little {affectionate_term} having trouble reaching the keyboard~?"
],
"overflow": [
"you've been a bad little {affectionate_term} and worn out {role}~"
]
},
"yikes": {
@@ -71,7 +97,10 @@
"{role} is getting hot~",
"that's a good {denigrating_term}~",
"yes~\nyes~~\nyes~~~",
"{role}'s going to keep {pronoun} good little {denigrating_term}~"
"{role}'s going to keep {pronoun} good little {denigrating_term}~",
"open wide {denigrating_term}.\nyou've earned {role}'s {part}~",
"do you want {role}'s {part}?\nkeep this up and you'll earn it~",
"oooh~ what a good {denigrating_term} you are~"
],
"negative": [
"you filthy {denigrating_term}~\nyou made a mess, now clean it up~\nwith your tongue~",
@@ -83,7 +112,14 @@
"{role} doesn't think {pronoun} little {denigrating_term} should have permission to wear clothes anymore~",
"never forget you belong to {role}~",
"does {role} need to put you in the {denigrating_term} wiggler~?",
"{role} is starting to wonder if you should just give up and become {pronoun} breeding stock~"
"{role} is starting to wonder if you should just give up and become {pronoun} breeding stock~",
"on your knees {denigrating_term}~",
"oh dear. {role} is not pleased",
"one spank per error sounds appropriate, don't you think {denigrating_term}?",
"no more {part} for you {denigrating_term}"
],
"overflow": [
"brats like you don't get to talk to {role}"
]
}
},
@@ -108,9 +144,7 @@
]
},
"role": {
"defaults": [
"mommy"
]
"defaults": []
},
"affectionate_term": {
"defaults": [

View File

@@ -9,11 +9,12 @@ import requests
from .static import get_config_file
mommy_logger = logging.getLogger("mommy")
serious_logger = logging.getLogger("serious")
logger = logging.Logger(__name__)
PREFIX = "MOMMY"
RESPONSES_URL = "https://raw.githubusercontent.com/diamondburned/go-mommy/refs/heads/main/responses.json"
RESPONSES_URL = "https://raw.githubusercontent.com/Gankra/cargo-mommy/refs/heads/main/responses.json"
RESPONSES_FILE = Path(__file__).parent / "responses.json"
ADDITIONAL_ENV_VARS = {
"pronoun": "PRONOUNS",
@@ -24,10 +25,10 @@ ADDITIONAL_ENV_VARS = {
def _load_config_file(config_file: Path) -> Dict[str, List[str]]:
def _load_config_file(config_file: Path) -> dict:
with config_file.open("r") as f:
data = toml.load(f)
result = {}
for key, value in data.items():
if isinstance(value, str):
@@ -42,7 +43,7 @@ ADDITIONAL_PROGRAM_PREFIXES = [
"cargo", # only as fallback if user already configured cargo
]
def _get_env_var_names(name: str):
def _get_env_var_names(name: str):
BASE = PREFIX + "_" + name.upper()
yield "PYTHON_" + BASE
yield BASE
@@ -55,24 +56,27 @@ def _get_env_value(name: str) -> Optional[str]:
val = os.environ.get(key)
if val is not None:
return val
for key in _get_env_var_names(name):
val = os.environ.get(key)
if val is not None:
return val
def compile_config(disable_requests: bool = False) -> dict:
global RESPONSES_FILE, RESPONSES_URL
data = json.loads(RESPONSES_FILE.read_text())
if not disable_requests:
print("mommy downloads newest responses for her girl~")
print(RESPONSES_URL)
print()
r = requests.get(RESPONSES_URL)
data = r.json()
mommy_logger.info("mommy downloads newest responses for her girl~ %s", RESPONSES_URL)
serious_logger.info("downloading cargo mommy responses: %s", RESPONSES_URL)
try:
r = requests.get(RESPONSES_URL)
data = r.json()
except requests.exceptions.ConnectionError:
mommy_logger.info("mommy couldn't fetch the url~")
serious_logger.info("couldnt fetch the url")
config_definition: Dict[str, dict] = data["vars"]
mood_definitions: Dict[str, dict] = data["moods"]
@@ -91,7 +95,22 @@ def compile_config(disable_requests: bool = False) -> dict:
# load config file
config_file = get_config_file()
if config_file is not None:
config.update(_load_config_file(config_file))
c = _load_config_file(config_file)
serious_logger.debug(
"config at %s:\n%s\n",
config_file,
json.dumps(c, indent=4)
)
config["mood"] = c.get("moods", config["mood"])
c_vars: dict = c.get("vars", {})
# validate the config var values
for key, val in c_vars.items():
if not isinstance(val, list):
mommy_logger.error("mommy needs the value of %s to be a list~", key)
serious_logger.error("the value of %s is not a list", key)
exit(1)
config.update(c_vars)
# fill config with env
for key, conf in config_definition.items():
@@ -99,11 +118,39 @@ def compile_config(disable_requests: bool = False) -> dict:
if val is not None:
config[key] = val.split("/")
# validate empty variables
empty_values = []
for key, value in config.items():
if len(value) == 0:
empty_values.append(key)
if len(empty_values) > 0:
empty_values_sting = ", ".join(empty_values)
mommy_logger.error(
"mommy is very displeased that you didn't config the key(s) %s",
empty_values_sting,
)
serious_logger.error(
"the following keys have empty values and need to be configured: %s",
empty_values_sting
)
exit(1)
# validate moods
for mood in config["mood"]:
if mood not in mood_definitions:
supported_moods_str = ", ".join(mood_definitions.keys())
print(f"{random.choice(config['role'])} doesn't know how to feel {mood}... {random.choice(config['pronoun'])} moods are {supported_moods_str}")
mommy_logger.error(
"%s doesn't know how to feel %s... %s moods are %s",
random.choice(config['role']),
mood,
random.choice(config['pronoun']),
supported_moods_str,
)
serious_logger.error(
"mood '%s' doesn't exist. moods are %s",
mood,
supported_moods_str,
)
exit(1)
# compile

View File

@@ -4,6 +4,7 @@ from pathlib import Path
import os
import logging
from typing import Optional
import sys
logger = logging.Logger(__name__)
@@ -72,12 +73,36 @@ def _get_xdg_config_dir() -> Path:
CONFIG_DIRECTORY = _get_xdg_config_dir() / "mommy"
COMPILED_CONFIG_FILE_NAME = "compiled-mommy.json"
IS_VENV = sys.prefix != sys.base_prefix
VENV_DIRECTORY = Path(sys.prefix)
def get_config_file() -> Optional[Path]:
config_files = [
config_files = []
if IS_VENV:
config_files.extend([
VENV_DIRECTORY / "python-mommy.toml",
VENV_DIRECTORY / "mommy.toml",
])
config_files.extend([
CONFIG_DIRECTORY / "python-mommy.toml",
CONFIG_DIRECTORY / "mommy.toml",
]
])
for f in config_files:
if f.exists():
return f
def get_compiled_config_file() -> Path:
compiled_config_files = [
VENV_DIRECTORY / "compiled-mommy.json",
CONFIG_DIRECTORY / "compiled-mommy.json",
]
for f in compiled_config_files:
if f.exists():
return f
raise Exception("couldn't find compiled config file")