Prepare public release with MIT license and private user storage
This commit is contained in:
parent
41772b4d1a
commit
eb8932134a
12 changed files with 175 additions and 26 deletions
|
|
@ -61,7 +61,7 @@ class Grocy:
|
|||
class Journal:
|
||||
"""Commit intent before each write; ambiguous writes require explicit reconciliation."""
|
||||
def __init__(self, path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
self.db = sqlite3.connect(path)
|
||||
self.db.execute("""CREATE TABLE IF NOT EXISTS imports (
|
||||
server TEXT, order_id TEXT, line INTEGER, fingerprint TEXT,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import click
|
|||
from lxml import html
|
||||
|
||||
from .grocy import Grocy, Journal, import_receipt, refresh_imported_products, align_recorded_lines
|
||||
from .paths import config_file, archive_directory, browser_directory, journal_file
|
||||
from .pantry import classify
|
||||
from .receipt import ImportError, parse_document, parse_ocado_order, receipt_date
|
||||
|
||||
|
|
@ -228,9 +229,7 @@ def run_import(receipts, config, config_path, archive, dry_run, failures, echo=p
|
|||
if not url or not key:
|
||||
raise ImportError("Configure Grocy URL and API key before importing")
|
||||
api = Grocy(url,key)
|
||||
state = Path(settings.get("state_file", "imports.sqlite3"))
|
||||
if not state.is_absolute():
|
||||
state = config_path.resolve().parent / state
|
||||
state = journal_file(config, config_path)
|
||||
journal = Journal(state)
|
||||
for receipt in receipts:
|
||||
if not dry_run:
|
||||
|
|
@ -266,9 +265,9 @@ def run_import(receipts, config, config_path, archive, dry_run, failures, echo=p
|
|||
|
||||
|
||||
@click.command(context_settings={"help_option_names":["-h","--help"]})
|
||||
@click.option("--config", "config_path", type=click.Path(path_type=Path,dir_okay=False),default="config.toml",show_default=True)
|
||||
@click.option("--profile", type=click.Path(path_type=Path,file_okay=False),default=".ocado-browser",show_default=True)
|
||||
@click.option("--archive", type=click.Path(path_type=Path,file_okay=False),default="history",show_default=True)
|
||||
@click.option("--config", "config_path", type=click.Path(path_type=Path,dir_okay=False),default=config_file,show_default="~/.config/ocado-grocy/config.toml")
|
||||
@click.option("--profile", type=click.Path(path_type=Path,file_okay=False),default=browser_directory,show_default="~/.local/state/ocado-grocy/browser")
|
||||
@click.option("--archive", type=click.Path(path_type=Path,file_okay=False),default=archive_directory,show_default="~/.local/share/ocado-grocy/history")
|
||||
@click.option("--since", help="Only orders delivered on or after YYYY-MM-DD.")
|
||||
@click.option("--limit", type=click.IntRange(min=1),help="Process at most this many new delivered orders (or explicitly selected orders).")
|
||||
@click.option("--dry-run", is_flag=True,help="Download and report selections without writing to Grocy.")
|
||||
|
|
@ -296,9 +295,7 @@ def main(config_path,profile,archive,since,limit,dry_run,cached,login_only,login
|
|||
connection = config.get('grocy', {})
|
||||
server = Grocy(os.environ.get('GROCY_URL', connection.get('url', '')),
|
||||
os.environ.get('GROCY_API_KEY', connection.get('api_key', ''))).url
|
||||
state = Path(config.get('import', {}).get('state_file', 'imports.sqlite3'))
|
||||
if not state.is_absolute():
|
||||
state = config_path.resolve().parent / state
|
||||
state = journal_file(config, config_path)
|
||||
completed = completed_orders(state, server, archive, config)
|
||||
if not dry_run and not login_only:
|
||||
mark_orders(state, server, completed, 'complete')
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import click
|
|||
import requests
|
||||
|
||||
from .grocy import Grocy, imported_products
|
||||
from .paths import config_file, archive_directory, journal_file
|
||||
from .history import private_json
|
||||
from .product_metadata import read_metadata
|
||||
|
||||
|
|
@ -72,15 +73,15 @@ def add_picture(client, downloader, product):
|
|||
|
||||
|
||||
@click.command()
|
||||
@click.option('--config', type=click.Path(path_type=Path), default=Path('config.toml'))
|
||||
@click.option('--report', type=click.Path(path_type=Path), default=Path('history/images/report.json'))
|
||||
@click.option('--config', type=click.Path(path_type=Path), default=config_file, show_default='~/.config/ocado-grocy/config.toml')
|
||||
@click.option('--report', type=click.Path(path_type=Path), default=lambda: archive_directory() / 'images/report.json', show_default='archive/images/report.json')
|
||||
@click.option('--apply', is_flag=True, help='Upload and assign missing product pictures.')
|
||||
@click.option('--order-id', 'order_ids', multiple=True, help='Limit to products imported from these orders.')
|
||||
def main(config, report, apply, order_ids):
|
||||
"""Add Ocado images to journal-linked products; keep existing pictures."""
|
||||
settings = tomllib.loads(config.read_text())
|
||||
client = Grocy(**settings['grocy'])
|
||||
state = config.parent / settings.get('import', {}).get('state_file', 'imports.sqlite3')
|
||||
state = journal_file(settings, config)
|
||||
products = imported_products(client, state, order_ids)
|
||||
return run_images(client, products, report, apply=apply)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import click
|
|||
import requests
|
||||
|
||||
from .grocy import Grocy, imported_products
|
||||
from .paths import config_file, archive_directory, journal_file
|
||||
from .history import private_json
|
||||
from .product_metadata import read_metadata, replace_metadata
|
||||
|
||||
|
|
@ -166,8 +167,8 @@ def apply_match(client, product, candidate, barcodes):
|
|||
|
||||
|
||||
@click.command()
|
||||
@click.option('--config', type=click.Path(path_type=Path), default=Path('config.toml'))
|
||||
@click.option('--cache', type=click.Path(path_type=Path), default=Path('history/openfoodfacts'))
|
||||
@click.option('--config', type=click.Path(path_type=Path), default=config_file, show_default='~/.config/ocado-grocy/config.toml')
|
||||
@click.option('--cache', type=click.Path(path_type=Path), default=lambda: archive_directory() / 'openfoodfacts', show_default='archive/openfoodfacts')
|
||||
@click.option('--contact', help='Contact for the Open Food Facts User-Agent; defaults to config [openfoodfacts].contact.')
|
||||
@click.option('--refresh-cache', is_flag=True, help='Fetch fresh OFF responses, replacing cached responses.')
|
||||
@click.option('--apply', is_flag=True, help='Apply unambiguous matches and explicitly reviewed mappings.')
|
||||
|
|
@ -184,7 +185,7 @@ def main(config, cache, contact, refresh_cache, apply, offline, mapping, limit,
|
|||
if refresh_cache and offline:
|
||||
raise click.UsageError('--refresh-cache cannot be combined with --offline')
|
||||
client = Grocy(**settings['grocy'])
|
||||
state = config.parent / settings.get('import', {}).get('state_file', 'imports.sqlite3')
|
||||
state = journal_file(settings, config)
|
||||
products = imported_products(client, state, order_ids)
|
||||
mappings = json.loads(mapping.read_text()) if mapping else {}
|
||||
return run_openfoodfacts(client, products, cache, contact, apply=apply, offline=offline,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def completed_orders(state, server, archive, config):
|
|||
def mark_orders(state, server, orders, status):
|
||||
if not orders:
|
||||
return
|
||||
state.parent.mkdir(parents=True, exist_ok=True)
|
||||
state.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
with sqlite3.connect(state) as db:
|
||||
db.execute('''CREATE TABLE IF NOT EXISTS loaded_orders (
|
||||
server TEXT NOT NULL, order_id TEXT NOT NULL, status TEXT NOT NULL,
|
||||
|
|
|
|||
44
ocado_grocy/paths.py
Normal file
44
ocado_grocy/paths.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""User-specific paths, with compatibility for existing checkout installations."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def user_directory(variable, fallback):
|
||||
value = os.environ.get(variable, '')
|
||||
# XDG paths must be absolute; ignore empty or relative values.
|
||||
base = Path(value) if value and Path(value).is_absolute() else Path.home() / fallback
|
||||
return base / 'ocado-grocy'
|
||||
|
||||
|
||||
def config_file():
|
||||
# Keep an existing installation on its original journal until migrated.
|
||||
legacy = Path('config.toml')
|
||||
return legacy if legacy.exists() else user_directory('XDG_CONFIG_HOME', '.config') / 'config.toml'
|
||||
|
||||
|
||||
def state_directory():
|
||||
return user_directory('XDG_STATE_HOME', '.local/state')
|
||||
|
||||
|
||||
def archive_directory():
|
||||
if Path('config.toml').exists():
|
||||
return Path('history')
|
||||
return user_directory('XDG_DATA_HOME', '.local/share') / 'history'
|
||||
|
||||
|
||||
def browser_directory():
|
||||
if Path('config.toml').exists():
|
||||
return Path('.ocado-browser')
|
||||
return state_directory() / 'browser'
|
||||
|
||||
|
||||
def journal_file(settings, config_path):
|
||||
configured = settings.get('import', {}).get('state_file')
|
||||
if configured:
|
||||
path = Path(configured).expanduser()
|
||||
return path if path.is_absolute() else config_path.resolve().parent / path
|
||||
user_config = user_directory('XDG_CONFIG_HOME', '.config') / 'config.toml'
|
||||
if config_path.resolve() == user_config.resolve():
|
||||
return state_directory() / 'imports.sqlite3'
|
||||
# Explicit custom/legacy configs retain the historical relative default.
|
||||
return config_path.resolve().parent / 'imports.sqlite3'
|
||||
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||
import click
|
||||
|
||||
from .grocy import Grocy, imported_products
|
||||
from .paths import journal_file
|
||||
from .history import private_json
|
||||
from .images import run_images
|
||||
from .openfoodfacts import run_openfoodfacts
|
||||
|
|
@ -18,9 +19,7 @@ def enrich_orders(config, config_path, archive, order_ids, *, images=True, openf
|
|||
connection = config.get('grocy', {})
|
||||
client = Grocy(os.environ.get('GROCY_URL', connection.get('url', '')),
|
||||
os.environ.get('GROCY_API_KEY', connection.get('api_key', '')))
|
||||
state = Path(config.get('import', {}).get('state_file', 'imports.sqlite3'))
|
||||
if not state.is_absolute():
|
||||
state = config_path.resolve().parent / state
|
||||
state = journal_file(config, config_path)
|
||||
products = imported_products(client, state, order_ids)
|
||||
key = order_ids[0] if len(order_ids) == 1 and order_ids[0].isdigit() else hashlib.sha256(','.join(sorted(order_ids)).encode()).hexdigest()[:16]
|
||||
output = archive / 'enrichment' / key
|
||||
|
|
@ -65,7 +64,7 @@ def retry_enrichment(config, config_path, archive, *, images=True, openfoodfacts
|
|||
connection = config.get('grocy', {})
|
||||
client = Grocy(os.environ.get('GROCY_URL', connection.get('url','')),
|
||||
os.environ.get('GROCY_API_KEY', connection.get('api_key','')))
|
||||
state = config_path.resolve().parent / config.get('import',{}).get('state_file','imports.sqlite3')
|
||||
state = journal_file(config, config_path)
|
||||
if not state.exists():
|
||||
return {'errors':[]}
|
||||
queue = RetryQueue(state, client.url)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue