oh-my-common 0.1.0__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.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: oh-my-common
3
+ Version: 0.1.0
4
+ Summary: Shared runtime for the oh-my-* Flask services: settings DB, Claude CLI gateway, timezone, base-url, scheduler.
5
+ Author-email: Dennis L <dennisl@udel.edu>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ByteDennis/oh-my-common
8
+ Keywords: oh-my,flask,claude,settings
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Framework :: Flask
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: flask
15
+ Requires-Dist: flask>=3.0; extra == "flask"
16
+ Provides-Extra: scheduler
17
+ Requires-Dist: apscheduler>=3.10; extra == "scheduler"
18
+
19
+ # oh-my-common
20
+
21
+ Shared runtime for the `oh-my-*` Flask services. Extracted so each split-out
22
+ service (oh-my-rss / oh-my-msg / oh-my-write) shares one implementation instead
23
+ of drifting copies. Per the split design, each service keeps its **own**
24
+ settings DB — this package is the **code** layer, not a shared store.
25
+
26
+ ## Modules
27
+
28
+ - **`settings`** — SQLite settings client + secret resolution.
29
+ `get_setting/put_setting/get_all`, the `provider_tokens` cache
30
+ (`get/set/clear_cached_token`), and `resolve_secret(key, env_key)` with
31
+ precedence **service-namespace > `global` > env**. Convenience:
32
+ `resolve_claude_token()`, `resolve_openai_key()`.
33
+ Config via env: `SETTINGS_DB` (path), `OMI_SERVICE_NS` (this service's
34
+ namespace, e.g. `clipboard`).
35
+ - **`claude`** — Claude CLI gateway: `resolve_claude_bin`, `claude_text`,
36
+ `run_claude_capture`, `gate_ai`, `claude_status`, `extract_json`,
37
+ `estimate_tokens`, `estimate_cost`, `MODEL_PRICING`. Flask-free.
38
+ - **`tz`** — `local_tz()/now_local()/today_local()` from `OMI_TZ_OFFSET` (hours, default +8).
39
+ - **`urls`** — `request_base_url()` (honors `X-Forwarded-Proto/Host`). Imports flask lazily.
40
+ - **`scheduler`** — `make_scheduler(ENABLED_ENV)` → a guarded `BackgroundScheduler`
41
+ (returns `None` when `ENABLED_ENV=0`, so extra gunicorn workers don't double-run).
42
+ Imports apscheduler lazily.
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ import os
48
+ os.environ.setdefault('OMI_SERVICE_NS', 'clipboard')
49
+ from oh_my_common import get_setting, claude_text, resolve_openai_key, now_local
50
+
51
+ key, src = resolve_openai_key()
52
+ text = claude_text('Summarize: ...', model='sonnet')
53
+ ```
54
+
55
+ ## Install
56
+
57
+ ```
58
+ pip install oh-my-common # core (stdlib only)
59
+ pip install 'oh-my-common[flask,scheduler]' # + request_base_url + make_scheduler
60
+ ```
61
+
62
+ Distribution into Docker services is TBD — see the migration plan. Options:
63
+ publish to a registry, vendor into each monorepo, or a git dependency.
@@ -0,0 +1,45 @@
1
+ # oh-my-common
2
+
3
+ Shared runtime for the `oh-my-*` Flask services. Extracted so each split-out
4
+ service (oh-my-rss / oh-my-msg / oh-my-write) shares one implementation instead
5
+ of drifting copies. Per the split design, each service keeps its **own**
6
+ settings DB — this package is the **code** layer, not a shared store.
7
+
8
+ ## Modules
9
+
10
+ - **`settings`** — SQLite settings client + secret resolution.
11
+ `get_setting/put_setting/get_all`, the `provider_tokens` cache
12
+ (`get/set/clear_cached_token`), and `resolve_secret(key, env_key)` with
13
+ precedence **service-namespace > `global` > env**. Convenience:
14
+ `resolve_claude_token()`, `resolve_openai_key()`.
15
+ Config via env: `SETTINGS_DB` (path), `OMI_SERVICE_NS` (this service's
16
+ namespace, e.g. `clipboard`).
17
+ - **`claude`** — Claude CLI gateway: `resolve_claude_bin`, `claude_text`,
18
+ `run_claude_capture`, `gate_ai`, `claude_status`, `extract_json`,
19
+ `estimate_tokens`, `estimate_cost`, `MODEL_PRICING`. Flask-free.
20
+ - **`tz`** — `local_tz()/now_local()/today_local()` from `OMI_TZ_OFFSET` (hours, default +8).
21
+ - **`urls`** — `request_base_url()` (honors `X-Forwarded-Proto/Host`). Imports flask lazily.
22
+ - **`scheduler`** — `make_scheduler(ENABLED_ENV)` → a guarded `BackgroundScheduler`
23
+ (returns `None` when `ENABLED_ENV=0`, so extra gunicorn workers don't double-run).
24
+ Imports apscheduler lazily.
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ import os
30
+ os.environ.setdefault('OMI_SERVICE_NS', 'clipboard')
31
+ from oh_my_common import get_setting, claude_text, resolve_openai_key, now_local
32
+
33
+ key, src = resolve_openai_key()
34
+ text = claude_text('Summarize: ...', model='sonnet')
35
+ ```
36
+
37
+ ## Install
38
+
39
+ ```
40
+ pip install oh-my-common # core (stdlib only)
41
+ pip install 'oh-my-common[flask,scheduler]' # + request_base_url + make_scheduler
42
+ ```
43
+
44
+ Distribution into Docker services is TBD — see the migration plan. Options:
45
+ publish to a registry, vendor into each monorepo, or a git dependency.
@@ -0,0 +1,28 @@
1
+ from .settings import (
2
+ SETTINGS_DB, SERVICE_NS,
3
+ init_settings_db, seed_global_settings_from_env,
4
+ get_setting, put_setting, get_all,
5
+ init_token_cache, get_cached_token, set_cached_token, clear_cached_token,
6
+ resolve_secret, resolve_claude_token, resolve_openai_key,
7
+ )
8
+ from .claude import (
9
+ MODEL_PRICING, resolve_claude_bin, claude_env, claude_status, gate_ai,
10
+ run_claude_capture, claude_text, extract_json, estimate_tokens, estimate_cost,
11
+ )
12
+ from .tz import local_tz, now_local, today_local
13
+ from .urls import request_base_url
14
+ from .scheduler import make_scheduler
15
+
16
+ __version__ = '0.1.0'
17
+
18
+ __all__ = [
19
+ 'SETTINGS_DB', 'SERVICE_NS',
20
+ 'init_settings_db', 'seed_global_settings_from_env',
21
+ 'get_setting', 'put_setting', 'get_all',
22
+ 'init_token_cache', 'get_cached_token', 'set_cached_token', 'clear_cached_token',
23
+ 'resolve_secret', 'resolve_claude_token', 'resolve_openai_key',
24
+ 'MODEL_PRICING', 'resolve_claude_bin', 'claude_env', 'claude_status', 'gate_ai',
25
+ 'run_claude_capture', 'claude_text', 'extract_json', 'estimate_tokens', 'estimate_cost',
26
+ 'local_tz', 'now_local', 'today_local',
27
+ 'request_base_url', 'make_scheduler',
28
+ ]
@@ -0,0 +1,102 @@
1
+ import glob
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+
7
+ from .settings import resolve_claude_token
8
+
9
+ MODEL_PRICING = {
10
+ 'haiku': (1.0, 5.0),
11
+ 'sonnet': (3.0, 15.0),
12
+ 'opus': (15.0, 75.0),
13
+ }
14
+
15
+
16
+ # >>> locate the claude binary: $CLAUDE_BIN > PATH > newest /opt/claude/versions/* <<< #
17
+ def resolve_claude_bin() -> str:
18
+ if os.environ.get('CLAUDE_BIN'):
19
+ return os.environ['CLAUDE_BIN']
20
+ on_path = shutil.which('claude')
21
+ if on_path:
22
+ return on_path
23
+ cands = sorted(glob.glob('/opt/claude/versions/*'), key=os.path.getmtime)
24
+ return cands[-1] if cands else ''
25
+
26
+
27
+ # >>> subprocess env carrying the OAuth token <<< #
28
+ def claude_env(token: str) -> dict:
29
+ env = os.environ.copy()
30
+ env['HOME'] = '/root'
31
+ env['CLAUDE_CODE_OAUTH_TOKEN'] = token
32
+ return env
33
+
34
+
35
+ # >>> readiness of the Claude gateway: {configured, has_token, has_binary} <<< #
36
+ def claude_status(service_ns: str = None) -> dict:
37
+ token, _src = resolve_claude_token(service_ns)
38
+ claude_bin = resolve_claude_bin()
39
+ return {'configured': bool(token and claude_bin),
40
+ 'has_token': bool(token), 'has_binary': bool(claude_bin)}
41
+
42
+
43
+ # >>> (token, claude_bin) if ready else (None, None) <<< #
44
+ def gate_ai(service_ns: str = None):
45
+ token, _src = resolve_claude_token(service_ns)
46
+ claude_bin = resolve_claude_bin()
47
+ if not token or not claude_bin:
48
+ return None, None
49
+ return token, claude_bin
50
+
51
+
52
+ # >>> run `claude -p` once, return full stdout (blocking); raises on non-zero <<< #
53
+ def run_claude_capture(claude_bin, token, model, prompt, timeout=180):
54
+ proc = subprocess.run(
55
+ [claude_bin, '-p', '--model', model],
56
+ input=prompt, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
57
+ text=True, env=claude_env(token), timeout=timeout,
58
+ )
59
+ if proc.returncode != 0:
60
+ raise RuntimeError(proc.stderr.strip() or 'claude exited non-zero')
61
+ return proc.stdout
62
+
63
+
64
+ # >>> one-shot Claude call; resolves the token if not supplied <<< #
65
+ def claude_text(prompt, model='sonnet', service_ns: str = None, timeout=180):
66
+ token, claude_bin = gate_ai(service_ns)
67
+ if not token or not claude_bin:
68
+ raise RuntimeError('Claude not configured')
69
+ return run_claude_capture(claude_bin, token, model, prompt, timeout).strip()
70
+
71
+
72
+ # >>> pull the first balanced JSON object out of a model reply (tolerates fences/prose) <<< #
73
+ def extract_json(s):
74
+ s = s.strip()
75
+ if s.startswith('```'):
76
+ s = s.split('\n', 1)[-1]
77
+ if s.endswith('```'):
78
+ s = s[:-3]
79
+ start = s.find('{')
80
+ if start == -1:
81
+ raise ValueError('no JSON object in reply')
82
+ depth = 0
83
+ for i in range(start, len(s)):
84
+ c = s[i]
85
+ if c == '{':
86
+ depth += 1
87
+ elif c == '}':
88
+ depth -= 1
89
+ if depth == 0:
90
+ return json.loads(s[start:i + 1])
91
+ raise ValueError('unbalanced JSON in reply')
92
+
93
+
94
+ # >>> rough token estimate (~4 chars/token) for cost accounting <<< #
95
+ def estimate_tokens(text) -> int:
96
+ return max(1, len(text or '') // 4)
97
+
98
+
99
+ # >>> USD cost of a call given char/token counts and model pricing <<< #
100
+ def estimate_cost(tok_in, tok_out, model='sonnet') -> float:
101
+ pin, pout = MODEL_PRICING.get(model, (3.0, 15.0))
102
+ return tok_in / 1e6 * pin + tok_out / 1e6 * pout
@@ -0,0 +1,11 @@
1
+ import os
2
+
3
+
4
+ # >>> BackgroundScheduler guarded by an env flag (set <FLAG>=0 on extra gunicorn workers to avoid duplicate runs); returns None when disabled <<< #
5
+ def make_scheduler(enabled_env: str, timezone_=None):
6
+ if os.environ.get(enabled_env, '1') == '0':
7
+ return None
8
+ from apscheduler.schedulers.background import BackgroundScheduler
9
+ from .tz import local_tz
10
+ sched = BackgroundScheduler(timezone=timezone_ or local_tz())
11
+ return sched
@@ -0,0 +1,168 @@
1
+ import os
2
+ import sqlite3
3
+
4
+ SETTINGS_DB = os.environ.get('SETTINGS_DB', '/data/oh-my-clipboard.db')
5
+ SERVICE_NS = os.environ.get('OMI_SERVICE_NS', 'app')
6
+
7
+ ENV_MAP = {
8
+ 'claude_code_oauth_token': 'CLAUDE_CODE_OAUTH_TOKEN',
9
+ 'openai_api_key': 'OPENAI_API_KEY',
10
+ }
11
+
12
+
13
+ # >>> sqlite connection to the settings DB <<< #
14
+ def _conn():
15
+ conn = sqlite3.connect(SETTINGS_DB)
16
+ conn.row_factory = sqlite3.Row
17
+ return conn
18
+
19
+
20
+ # >>> create the settings table if missing <<< #
21
+ def init_settings_db():
22
+ os.makedirs(os.path.dirname(SETTINGS_DB), exist_ok=True)
23
+ conn = sqlite3.connect(SETTINGS_DB)
24
+ conn.execute('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)')
25
+ conn.commit()
26
+ conn.close()
27
+
28
+
29
+ # >>> seed global.* settings from env on first boot (only when unset) <<< #
30
+ def seed_global_settings_from_env():
31
+ init_settings_db()
32
+ conn = _conn()
33
+ for key, env_key in ENV_MAP.items():
34
+ val = os.environ.get(env_key, '')
35
+ if not val:
36
+ continue
37
+ existing = conn.execute(
38
+ 'SELECT value FROM settings WHERE key = ?', (f'global.{key}',)
39
+ ).fetchone()
40
+ if not existing or not existing['value']:
41
+ conn.execute(
42
+ 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
43
+ (f'global.{key}', val),
44
+ )
45
+ conn.commit()
46
+ conn.close()
47
+
48
+
49
+ # >>> read one namespaced setting, falling back to default <<< #
50
+ def get_setting(namespace: str, key: str, default: str = '') -> str:
51
+ try:
52
+ conn = _conn()
53
+ row = conn.execute(
54
+ 'SELECT value FROM settings WHERE key = ?', (f'{namespace}.{key}',)
55
+ ).fetchone()
56
+ conn.close()
57
+ if row and row['value']:
58
+ return row['value']
59
+ except Exception:
60
+ pass
61
+ return default
62
+
63
+
64
+ # >>> write one namespaced setting <<< #
65
+ def put_setting(namespace: str, key: str, value: str):
66
+ init_settings_db()
67
+ conn = _conn()
68
+ conn.execute(
69
+ 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
70
+ (f'{namespace}.{key}', value or ''),
71
+ )
72
+ conn.commit()
73
+ conn.close()
74
+
75
+
76
+ # >>> all settings under a namespace as a {key: value} dict <<< #
77
+ def get_all(namespace: str) -> dict:
78
+ prefix = namespace + '.'
79
+ try:
80
+ conn = _conn()
81
+ rows = conn.execute(
82
+ 'SELECT key, value FROM settings WHERE key LIKE ?', (prefix + '%',)
83
+ ).fetchall()
84
+ conn.close()
85
+ return {r['key'][len(prefix):]: r['value'] for r in rows}
86
+ except Exception:
87
+ return {}
88
+
89
+
90
+ # >>> create the short-lived provider-token cache table <<< #
91
+ def init_token_cache():
92
+ os.makedirs(os.path.dirname(SETTINGS_DB), exist_ok=True)
93
+ conn = sqlite3.connect(SETTINGS_DB)
94
+ conn.execute(
95
+ 'CREATE TABLE IF NOT EXISTS provider_tokens ('
96
+ ' provider TEXT PRIMARY KEY, access_token TEXT NOT NULL,'
97
+ ' expires_at TEXT NOT NULL, meta_json TEXT)'
98
+ )
99
+ conn.commit()
100
+ conn.close()
101
+
102
+
103
+ # >>> cached bearer token for a provider, or None <<< #
104
+ def get_cached_token(provider: str):
105
+ try:
106
+ init_token_cache()
107
+ conn = _conn()
108
+ row = conn.execute(
109
+ 'SELECT access_token, expires_at, meta_json FROM provider_tokens WHERE provider = ?',
110
+ (provider,),
111
+ ).fetchone()
112
+ conn.close()
113
+ if row:
114
+ return {'access_token': row['access_token'], 'expires_at': row['expires_at'],
115
+ 'meta': row['meta_json'] or ''}
116
+ except Exception:
117
+ pass
118
+ return None
119
+
120
+
121
+ # >>> store a provider bearer token <<< #
122
+ def set_cached_token(provider: str, access_token: str, expires_at: str, meta: str = ''):
123
+ init_token_cache()
124
+ conn = _conn()
125
+ conn.execute(
126
+ 'INSERT OR REPLACE INTO provider_tokens (provider, access_token, expires_at, meta_json)'
127
+ ' VALUES (?, ?, ?, ?)',
128
+ (provider, access_token, expires_at, meta or ''),
129
+ )
130
+ conn.commit()
131
+ conn.close()
132
+
133
+
134
+ # >>> drop a provider's cached token <<< #
135
+ def clear_cached_token(provider: str):
136
+ try:
137
+ init_token_cache()
138
+ conn = _conn()
139
+ conn.execute('DELETE FROM provider_tokens WHERE provider = ?', (provider,))
140
+ conn.commit()
141
+ conn.close()
142
+ except Exception:
143
+ pass
144
+
145
+
146
+ # >>> resolve a secret: service namespace > global > env > '' ; returns (value, source) <<< #
147
+ def resolve_secret(key: str, env_key: str, service_ns: str = None) -> tuple[str, str]:
148
+ ns = service_ns or SERVICE_NS
149
+ val = get_setting(ns, key)
150
+ if val:
151
+ return val, 'user'
152
+ val = get_setting('global', key)
153
+ if val:
154
+ return val, 'global'
155
+ val = os.environ.get(env_key, '')
156
+ if val:
157
+ return val, 'env'
158
+ return '', ''
159
+
160
+
161
+ # >>> (token, source) for the Claude OAuth token <<< #
162
+ def resolve_claude_token(service_ns: str = None) -> tuple[str, str]:
163
+ return resolve_secret('claude_code_oauth_token', 'CLAUDE_CODE_OAUTH_TOKEN', service_ns)
164
+
165
+
166
+ # >>> (key, source) for the OpenAI API key <<< #
167
+ def resolve_openai_key(service_ns: str = None) -> tuple[str, str]:
168
+ return resolve_secret('openai_api_key', 'OPENAI_API_KEY', service_ns)
@@ -0,0 +1,21 @@
1
+ import os
2
+ from datetime import datetime, timedelta, timezone
3
+
4
+
5
+ # >>> fixed-offset tz from OMI_TZ_OFFSET (hours, default +8) for day boundaries / schedulers <<< #
6
+ def local_tz() -> timezone:
7
+ try:
8
+ hours = int(os.environ.get('OMI_TZ_OFFSET', '8'))
9
+ except (ValueError, TypeError):
10
+ hours = 8
11
+ return timezone(timedelta(hours=hours))
12
+
13
+
14
+ # >>> current local datetime <<< #
15
+ def now_local() -> datetime:
16
+ return datetime.now(local_tz())
17
+
18
+
19
+ # >>> current local date <<< #
20
+ def today_local():
21
+ return now_local().date()
@@ -0,0 +1,6 @@
1
+ # >>> base URL from the incoming request, honoring X-Forwarded-Proto/Host (for feed/media links) <<< #
2
+ def request_base_url() -> str:
3
+ from flask import request
4
+ scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
5
+ host = request.headers.get('X-Forwarded-Host', request.host)
6
+ return f'{scheme}://{host}'
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: oh-my-common
3
+ Version: 0.1.0
4
+ Summary: Shared runtime for the oh-my-* Flask services: settings DB, Claude CLI gateway, timezone, base-url, scheduler.
5
+ Author-email: Dennis L <dennisl@udel.edu>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ByteDennis/oh-my-common
8
+ Keywords: oh-my,flask,claude,settings
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Framework :: Flask
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: flask
15
+ Requires-Dist: flask>=3.0; extra == "flask"
16
+ Provides-Extra: scheduler
17
+ Requires-Dist: apscheduler>=3.10; extra == "scheduler"
18
+
19
+ # oh-my-common
20
+
21
+ Shared runtime for the `oh-my-*` Flask services. Extracted so each split-out
22
+ service (oh-my-rss / oh-my-msg / oh-my-write) shares one implementation instead
23
+ of drifting copies. Per the split design, each service keeps its **own**
24
+ settings DB — this package is the **code** layer, not a shared store.
25
+
26
+ ## Modules
27
+
28
+ - **`settings`** — SQLite settings client + secret resolution.
29
+ `get_setting/put_setting/get_all`, the `provider_tokens` cache
30
+ (`get/set/clear_cached_token`), and `resolve_secret(key, env_key)` with
31
+ precedence **service-namespace > `global` > env**. Convenience:
32
+ `resolve_claude_token()`, `resolve_openai_key()`.
33
+ Config via env: `SETTINGS_DB` (path), `OMI_SERVICE_NS` (this service's
34
+ namespace, e.g. `clipboard`).
35
+ - **`claude`** — Claude CLI gateway: `resolve_claude_bin`, `claude_text`,
36
+ `run_claude_capture`, `gate_ai`, `claude_status`, `extract_json`,
37
+ `estimate_tokens`, `estimate_cost`, `MODEL_PRICING`. Flask-free.
38
+ - **`tz`** — `local_tz()/now_local()/today_local()` from `OMI_TZ_OFFSET` (hours, default +8).
39
+ - **`urls`** — `request_base_url()` (honors `X-Forwarded-Proto/Host`). Imports flask lazily.
40
+ - **`scheduler`** — `make_scheduler(ENABLED_ENV)` → a guarded `BackgroundScheduler`
41
+ (returns `None` when `ENABLED_ENV=0`, so extra gunicorn workers don't double-run).
42
+ Imports apscheduler lazily.
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ import os
48
+ os.environ.setdefault('OMI_SERVICE_NS', 'clipboard')
49
+ from oh_my_common import get_setting, claude_text, resolve_openai_key, now_local
50
+
51
+ key, src = resolve_openai_key()
52
+ text = claude_text('Summarize: ...', model='sonnet')
53
+ ```
54
+
55
+ ## Install
56
+
57
+ ```
58
+ pip install oh-my-common # core (stdlib only)
59
+ pip install 'oh-my-common[flask,scheduler]' # + request_base_url + make_scheduler
60
+ ```
61
+
62
+ Distribution into Docker services is TBD — see the migration plan. Options:
63
+ publish to a registry, vendor into each monorepo, or a git dependency.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ oh_my_common/__init__.py
4
+ oh_my_common/claude.py
5
+ oh_my_common/scheduler.py
6
+ oh_my_common/settings.py
7
+ oh_my_common/tz.py
8
+ oh_my_common/urls.py
9
+ oh_my_common.egg-info/PKG-INFO
10
+ oh_my_common.egg-info/SOURCES.txt
11
+ oh_my_common.egg-info/dependency_links.txt
12
+ oh_my_common.egg-info/requires.txt
13
+ oh_my_common.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+
2
+ [flask]
3
+ flask>=3.0
4
+
5
+ [scheduler]
6
+ apscheduler>=3.10
@@ -0,0 +1 @@
1
+ oh_my_common
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oh-my-common"
7
+ version = "0.1.0"
8
+ description = "Shared runtime for the oh-my-* Flask services: settings DB, Claude CLI gateway, timezone, base-url, scheduler."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Dennis L", email = "dennisl@udel.edu" }]
13
+ keywords = ["oh-my", "flask", "claude", "settings"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Framework :: Flask",
18
+ ]
19
+ dependencies = []
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/ByteDennis/oh-my-common"
23
+
24
+ [project.optional-dependencies]
25
+ flask = ["flask>=3.0"]
26
+ scheduler = ["apscheduler>=3.10"]
27
+
28
+ [tool.setuptools]
29
+ packages = ["oh_my_common"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+