peekview 0.1.0__py3-none-any.whl
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.
- peek/__init__.py +3 -0
- peek/__main__.py +6 -0
- peek/api/__init__.py +1 -0
- peek/api/entries.py +123 -0
- peek/api/files.py +114 -0
- peek/cli.py +332 -0
- peek/config.py +232 -0
- peek/database.py +304 -0
- peek/exceptions.py +127 -0
- peek/language.py +407 -0
- peek/main.py +224 -0
- peek/models.py +330 -0
- peek/services/__init__.py +1 -0
- peek/services/entry_service.py +612 -0
- peek/services/file_service.py +231 -0
- peek/static/assets/EntryDetailView-Cnhdjikj.css +1 -0
- peek/static/assets/EntryDetailView-yI3ydvJn.js +77 -0
- peek/static/assets/EntryListView-CjYcs8e1.js +1 -0
- peek/static/assets/EntryListView-DjPX64rQ.css +1 -0
- peek/static/assets/abap-DsBKuouk.js +1 -0
- peek/static/assets/actionscript-3-D_z4Izcz.js +1 -0
- peek/static/assets/ada-727ZlQH0.js +1 -0
- peek/static/assets/andromeeda-C3khCPGq.js +1 -0
- peek/static/assets/angular-html-LfdN0zeE.js +1 -0
- peek/static/assets/angular-ts-CKsD7JZE.js +1 -0
- peek/static/assets/apache-Dn00JSTd.js +1 -0
- peek/static/assets/apex-COJ4H7py.js +1 -0
- peek/static/assets/apl-BBq3IX1j.js +1 -0
- peek/static/assets/applescript-Bu5BbsvL.js +1 -0
- peek/static/assets/ara-7O62HKoU.js +1 -0
- peek/static/assets/asciidoc-BPT9niGB.js +1 -0
- peek/static/assets/asm-Dhn9LcZ4.js +1 -0
- peek/static/assets/astro-CqkE3fuf.js +1 -0
- peek/static/assets/aurora-x-D-2ljcwZ.js +1 -0
- peek/static/assets/awk-eg146-Ew.js +1 -0
- peek/static/assets/ayu-dark-Cv9koXgw.js +1 -0
- peek/static/assets/ballerina-Du268qiB.js +1 -0
- peek/static/assets/bat-fje9CFhw.js +1 -0
- peek/static/assets/beancount-BwXTMy5W.js +1 -0
- peek/static/assets/berry-3xVqZejG.js +1 -0
- peek/static/assets/bibtex-xW4inM5L.js +1 -0
- peek/static/assets/bicep-DHo0CJ0O.js +1 -0
- peek/static/assets/blade-a8OxSdnT.js +1 -0
- peek/static/assets/bsl-Dgyn0ogV.js +1 -0
- peek/static/assets/c-C3t2pwGQ.js +1 -0
- peek/static/assets/cadence-DNquZEk8.js +1 -0
- peek/static/assets/cairo--RitsXJZ.js +1 -0
- peek/static/assets/catppuccin-frappe-CD_QflpE.js +1 -0
- peek/static/assets/catppuccin-latte-DRW-0cLl.js +1 -0
- peek/static/assets/catppuccin-macchiato-C-_shW-Y.js +1 -0
- peek/static/assets/catppuccin-mocha-LGGdnPYs.js +1 -0
- peek/static/assets/clarity-BHOwM8T6.js +1 -0
- peek/static/assets/clojure-DxSadP1t.js +1 -0
- peek/static/assets/cmake-DbXoA79R.js +1 -0
- peek/static/assets/cobol-PTqiYgYu.js +1 -0
- peek/static/assets/codeowners-Bp6g37R7.js +1 -0
- peek/static/assets/codeql-sacFqUAJ.js +1 -0
- peek/static/assets/coffee-dyiR41kL.js +1 -0
- peek/static/assets/common-lisp-C7gG9l05.js +1 -0
- peek/static/assets/coq-Dsg_Bt_b.js +1 -0
- peek/static/assets/cpp-BksuvNSY.js +1 -0
- peek/static/assets/crystal-DtDmRg-F.js +1 -0
- peek/static/assets/csharp-D9R-vmeu.js +1 -0
- peek/static/assets/css-BPhBrDlE.js +1 -0
- peek/static/assets/csv-B0qRVHPH.js +1 -0
- peek/static/assets/cue-DtFQj3wx.js +1 -0
- peek/static/assets/cypher-m2LEI-9-.js +1 -0
- peek/static/assets/d-BoXegm-a.js +1 -0
- peek/static/assets/dark-plus-C3mMm8J8.js +1 -0
- peek/static/assets/dart-B9wLZaAG.js +1 -0
- peek/static/assets/dax-ClGRhx96.js +1 -0
- peek/static/assets/desktop-DEIpsLCJ.js +1 -0
- peek/static/assets/diff-BgYniUM_.js +1 -0
- peek/static/assets/docker-COcR7UxN.js +1 -0
- peek/static/assets/dotenv-BjQB5zDj.js +1 -0
- peek/static/assets/dracula-BzJJZx-M.js +1 -0
- peek/static/assets/dracula-soft-BXkSAIEj.js +1 -0
- peek/static/assets/dream-maker-C-nORZOA.js +1 -0
- peek/static/assets/edge-D5gP-w-T.js +1 -0
- peek/static/assets/elixir-CLiX3zqd.js +1 -0
- peek/static/assets/elm-CmHSxxaM.js +1 -0
- peek/static/assets/emacs-lisp-BX77sIaO.js +1 -0
- peek/static/assets/erb-BYTLMnw6.js +1 -0
- peek/static/assets/erlang-B-DoSBHF.js +1 -0
- peek/static/assets/everforest-dark-BgDCqdQA.js +1 -0
- peek/static/assets/everforest-light-C8M2exoo.js +1 -0
- peek/static/assets/fennel-bCA53EVm.js +1 -0
- peek/static/assets/fish-w-ucz2PV.js +1 -0
- peek/static/assets/fluent-Dayu4EKP.js +1 -0
- peek/static/assets/fortran-fixed-form-TqA4NnZg.js +1 -0
- peek/static/assets/fortran-free-form-DKXYxT9g.js +1 -0
- peek/static/assets/fsharp-XplgxFYe.js +1 -0
- peek/static/assets/gdresource-BHYsBjWJ.js +1 -0
- peek/static/assets/gdscript-DfxzS6Rs.js +1 -0
- peek/static/assets/gdshader-SKMF96pI.js +1 -0
- peek/static/assets/genie-ajMbGru0.js +1 -0
- peek/static/assets/gherkin--30QC5Em.js +1 -0
- peek/static/assets/git-commit-i4q6IMui.js +1 -0
- peek/static/assets/git-rebase-B-v9cOL2.js +1 -0
- peek/static/assets/github-dark-DHJKELXO.js +1 -0
- peek/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
- peek/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- peek/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- peek/static/assets/github-light-DAi9KRSo.js +1 -0
- peek/static/assets/github-light-default-D7oLnXFd.js +1 -0
- peek/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- peek/static/assets/gleam-B430Bg39.js +1 -0
- peek/static/assets/glimmer-js-D-cwc0-E.js +1 -0
- peek/static/assets/glimmer-ts-pgjy16dm.js +1 -0
- peek/static/assets/glsl-DBO2IWDn.js +1 -0
- peek/static/assets/gnuplot-CM8KxXT1.js +1 -0
- peek/static/assets/go-B1SYOhNW.js +1 -0
- peek/static/assets/graphql-cDcHW_If.js +1 -0
- peek/static/assets/groovy-DkBy-JyN.js +1 -0
- peek/static/assets/hack-D1yCygmZ.js +1 -0
- peek/static/assets/haml-B2EZWmdv.js +1 -0
- peek/static/assets/handlebars-BQGss363.js +1 -0
- peek/static/assets/haskell-BILxekzW.js +1 -0
- peek/static/assets/haxe-C5wWYbrZ.js +1 -0
- peek/static/assets/hcl-HzYwdGDm.js +1 -0
- peek/static/assets/hjson-T-Tgc4AT.js +1 -0
- peek/static/assets/hlsl-ifBTmRxC.js +1 -0
- peek/static/assets/houston-DnULxvSX.js +1 -0
- peek/static/assets/html-C2L_23MC.js +1 -0
- peek/static/assets/html-derivative-CSfWNPLT.js +1 -0
- peek/static/assets/http-FRrOvY1W.js +1 -0
- peek/static/assets/hxml-TIA70rKU.js +1 -0
- peek/static/assets/hy-BMj5Y0dO.js +1 -0
- peek/static/assets/imba-bv_oIlVt.js +1 -0
- peek/static/assets/index-CMhcFTfH.js +26 -0
- peek/static/assets/index-DvcXrXhI.css +1 -0
- peek/static/assets/ini-BjABl1g7.js +1 -0
- peek/static/assets/java-xI-RfyKK.js +1 -0
- peek/static/assets/javascript-ySlJ1b_l.js +1 -0
- peek/static/assets/jinja-DGy0s7-h.js +1 -0
- peek/static/assets/jison-BqZprYcd.js +1 -0
- peek/static/assets/json-BQoSv7ci.js +1 -0
- peek/static/assets/json5-w8dY5SsB.js +1 -0
- peek/static/assets/jsonc-TU54ms6u.js +1 -0
- peek/static/assets/jsonl-DREVFZK8.js +1 -0
- peek/static/assets/jsonnet-BfivnA6A.js +1 -0
- peek/static/assets/jssm-P4WzXJd0.js +1 -0
- peek/static/assets/jsx-BAng5TT0.js +1 -0
- peek/static/assets/julia-BBuGR-5E.js +1 -0
- peek/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- peek/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- peek/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
- peek/static/assets/kotlin-B5lbUyaz.js +1 -0
- peek/static/assets/kusto-mebxcVVE.js +1 -0
- peek/static/assets/laserwave-DUszq2jm.js +1 -0
- peek/static/assets/latex-C-cWTeAZ.js +1 -0
- peek/static/assets/lean-XBlWyCtg.js +1 -0
- peek/static/assets/less-BfCpw3nA.js +1 -0
- peek/static/assets/light-plus-B7mTdjB0.js +1 -0
- peek/static/assets/liquid-D3W5UaiH.js +1 -0
- peek/static/assets/log-Cc5clBb7.js +1 -0
- peek/static/assets/logo-IuBKFhSY.js +1 -0
- peek/static/assets/lua-CvWAzNxB.js +1 -0
- peek/static/assets/luau-Du5NY7AG.js +1 -0
- peek/static/assets/make-Bvotw-X0.js +1 -0
- peek/static/assets/markdown-UIAJJxZW.js +1 -0
- peek/static/assets/marko-z0MBrx5-.js +1 -0
- peek/static/assets/material-theme-D5KoaKCx.js +1 -0
- peek/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
- peek/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- peek/static/assets/material-theme-ocean-CyktbL80.js +1 -0
- peek/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- peek/static/assets/matlab-D9-PGadD.js +1 -0
- peek/static/assets/mdc-DB_EDNY_.js +1 -0
- peek/static/assets/mdx-sdHcTMYB.js +1 -0
- peek/static/assets/mermaid-Ci6OQyBP.js +1 -0
- peek/static/assets/min-dark-CafNBF8u.js +1 -0
- peek/static/assets/min-light-CTRr51gU.js +1 -0
- peek/static/assets/mipsasm-BC5c_5Pe.js +1 -0
- peek/static/assets/mojo-Tz6hzZYG.js +1 -0
- peek/static/assets/monokai-D4h5O-jR.js +1 -0
- peek/static/assets/move-DB_GagMm.js +1 -0
- peek/static/assets/narrat-DLbgOhZU.js +1 -0
- peek/static/assets/nextflow-B0XVJmRM.js +1 -0
- peek/static/assets/nginx-D_VnBJ67.js +1 -0
- peek/static/assets/night-owl-C39BiMTA.js +1 -0
- peek/static/assets/nim-ZlGxZxc3.js +1 -0
- peek/static/assets/nix-shcSOmrb.js +1 -0
- peek/static/assets/nord-Ddv68eIx.js +1 -0
- peek/static/assets/nushell-D4Tzg5kh.js +1 -0
- peek/static/assets/objective-c-Deuh7S70.js +1 -0
- peek/static/assets/objective-cpp-BUEGK8hf.js +1 -0
- peek/static/assets/ocaml-BNioltXt.js +1 -0
- peek/static/assets/one-dark-pro-GBQ2dnAY.js +1 -0
- peek/static/assets/one-light-PoHY5YXO.js +1 -0
- peek/static/assets/pascal-JqZropPD.js +1 -0
- peek/static/assets/perl-CHQXSrWU.js +1 -0
- peek/static/assets/php-B5ebYQev.js +1 -0
- peek/static/assets/plastic-3e1v2bzS.js +1 -0
- peek/static/assets/plsql-LKU2TuZ1.js +1 -0
- peek/static/assets/po-BFLt1xDp.js +1 -0
- peek/static/assets/poimandres-CS3Unz2-.js +1 -0
- peek/static/assets/polar-DKykz6zU.js +1 -0
- peek/static/assets/postcss-B3ZDOciz.js +1 -0
- peek/static/assets/powerquery-CSHBycmS.js +1 -0
- peek/static/assets/powershell-BIEUsx6d.js +1 -0
- peek/static/assets/prisma-B48N-Iqd.js +1 -0
- peek/static/assets/prolog-BY-TUvya.js +1 -0
- peek/static/assets/proto-zocC4JxJ.js +1 -0
- peek/static/assets/pug-CM9l7STV.js +1 -0
- peek/static/assets/puppet-Cza_XSSt.js +1 -0
- peek/static/assets/purescript-Bg-kzb6g.js +1 -0
- peek/static/assets/python-DhUJRlN_.js +1 -0
- peek/static/assets/qml-D8XfuvdV.js +1 -0
- peek/static/assets/qmldir-C8lEn-DE.js +1 -0
- peek/static/assets/qss-DhMKtDLN.js +1 -0
- peek/static/assets/r-CwjWoCRV.js +1 -0
- peek/static/assets/racket-CzouJOBO.js +1 -0
- peek/static/assets/raku-B1bQXN8T.js +1 -0
- peek/static/assets/razor-CNLDkMZG.js +1 -0
- peek/static/assets/red-bN70gL4F.js +1 -0
- peek/static/assets/reg-5LuOXUq_.js +1 -0
- peek/static/assets/regexp-DWJ3fJO_.js +1 -0
- peek/static/assets/rel-DJlmqQ1C.js +1 -0
- peek/static/assets/riscv-QhoSD0DR.js +1 -0
- peek/static/assets/rose-pine-CmCqftbK.js +1 -0
- peek/static/assets/rose-pine-dawn-Ds-gbosJ.js +1 -0
- peek/static/assets/rose-pine-moon-CjDtw9vr.js +1 -0
- peek/static/assets/rst-4NLicBqY.js +1 -0
- peek/static/assets/ruby-DeZ3UC14.js +1 -0
- peek/static/assets/rust-Be6lgOlo.js +1 -0
- peek/static/assets/sas-BmTFh92c.js +1 -0
- peek/static/assets/sass-BJ4Li9vH.js +1 -0
- peek/static/assets/scala-DQVVAn-B.js +1 -0
- peek/static/assets/scheme-BJGe-b2p.js +1 -0
- peek/static/assets/scss-C31hgJw-.js +1 -0
- peek/static/assets/sdbl-BLhTXw86.js +1 -0
- peek/static/assets/shaderlab-B7qAK45m.js +1 -0
- peek/static/assets/shellscript-atvbtKCR.js +1 -0
- peek/static/assets/shellsession-C_rIy8kc.js +1 -0
- peek/static/assets/slack-dark-BthQWCQV.js +1 -0
- peek/static/assets/slack-ochin-DqwNpetd.js +1 -0
- peek/static/assets/smalltalk-DkLiglaE.js +1 -0
- peek/static/assets/snazzy-light-Bw305WKR.js +1 -0
- peek/static/assets/solarized-dark-DXbdFlpD.js +1 -0
- peek/static/assets/solarized-light-L9t79GZl.js +1 -0
- peek/static/assets/solidity-C1w2a3ep.js +1 -0
- peek/static/assets/soy-C-lX7w71.js +1 -0
- peek/static/assets/sparql-bYkjHRlG.js +1 -0
- peek/static/assets/splunk-Cf8iN4DR.js +1 -0
- peek/static/assets/sql-COK4E0Yg.js +1 -0
- peek/static/assets/ssh-config-BknIz3MU.js +1 -0
- peek/static/assets/stata-DorPZHa4.js +1 -0
- peek/static/assets/stylus-BeQkCIfX.js +1 -0
- peek/static/assets/svelte-MSaWC3Je.js +1 -0
- peek/static/assets/swift-BSxZ-RaX.js +1 -0
- peek/static/assets/synthwave-84-CbfX1IO0.js +1 -0
- peek/static/assets/system-verilog-C7L56vO4.js +1 -0
- peek/static/assets/systemd-CUnW07Te.js +1 -0
- peek/static/assets/talonscript-C1XDQQGZ.js +1 -0
- peek/static/assets/tasl-CQjiPCtT.js +1 -0
- peek/static/assets/tcl-DQ1-QYvQ.js +1 -0
- peek/static/assets/templ-dwX3ZSMB.js +1 -0
- peek/static/assets/terraform-BbSNqyBO.js +1 -0
- peek/static/assets/tex-rYs2v40G.js +1 -0
- peek/static/assets/tokyo-night-DBQeEorK.js +1 -0
- peek/static/assets/toml-CB2ApiWb.js +1 -0
- peek/static/assets/ts-tags-CipyTH0X.js +1 -0
- peek/static/assets/tsv-B_m7g4N7.js +1 -0
- peek/static/assets/tsx-B6W0miNI.js +1 -0
- peek/static/assets/turtle-BMR_PYu6.js +1 -0
- peek/static/assets/twig-NC5TFiHP.js +1 -0
- peek/static/assets/typescript-Dj6nwHGl.js +1 -0
- peek/static/assets/typespec-BpWG_bgh.js +1 -0
- peek/static/assets/typst-BVUVsWT6.js +1 -0
- peek/static/assets/useEntry-DSu-PUHy.js +1 -0
- peek/static/assets/useEntry-Dgrs0_hj.css +1 -0
- peek/static/assets/v-CAQ2eGtk.js +1 -0
- peek/static/assets/vala-BFOHcciG.js +1 -0
- peek/static/assets/vb-CdO5JTpU.js +1 -0
- peek/static/assets/verilog-CJaU5se_.js +1 -0
- peek/static/assets/vesper-BEBZ7ncR.js +1 -0
- peek/static/assets/vhdl-DYoNaHQp.js +1 -0
- peek/static/assets/viml-m4uW47V2.js +1 -0
- peek/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
- peek/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
- peek/static/assets/vitesse-light-CVO1_9PV.js +1 -0
- peek/static/assets/vue-BuYVFjOK.js +1 -0
- peek/static/assets/vue-html-xdeiXROB.js +1 -0
- peek/static/assets/vyper-nyqBNV6O.js +1 -0
- peek/static/assets/wasm-C6j12Q_x.js +1 -0
- peek/static/assets/wasm-CG6Dc4jp.js +1 -0
- peek/static/assets/wenyan-7A4Fjokl.js +1 -0
- peek/static/assets/wgsl-CB0Krxn9.js +1 -0
- peek/static/assets/wikitext-DCE3LsBG.js +1 -0
- peek/static/assets/wolfram-C3FkfJm5.js +1 -0
- peek/static/assets/xml-e3z08dGr.js +1 -0
- peek/static/assets/xsl-Dd0NUgwM.js +1 -0
- peek/static/assets/yaml-CVw76BM1.js +1 -0
- peek/static/assets/zenscript-HnGAYVZD.js +1 -0
- peek/static/assets/zig-BVz_zdnA.js +1 -0
- peek/static/index.html +26 -0
- peek/storage.py +498 -0
- peekview-0.1.0.dist-info/METADATA +171 -0
- peekview-0.1.0.dist-info/RECORD +302 -0
- peekview-0.1.0.dist-info/WHEEL +4 -0
- peekview-0.1.0.dist-info/entry_points.txt +2 -0
peek/__init__.py
ADDED
peek/__main__.py
ADDED
peek/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Peek API routes."""
|
peek/api/entries.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Entry CRUD API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Query, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
from peek.models import CreateEntryRequest, EntryUpdate
|
|
9
|
+
from peek.services.entry_service import EntryService, get_entry_service
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/api/v1/entries", tags=["entries"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_service(request: Request) -> EntryService:
|
|
15
|
+
"""Get EntryService from app state."""
|
|
16
|
+
return get_entry_service(request.app)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.post("", status_code=201)
|
|
20
|
+
async def create_entry(
|
|
21
|
+
data: CreateEntryRequest,
|
|
22
|
+
request: Request,
|
|
23
|
+
service: EntryService = Depends(_get_service),
|
|
24
|
+
):
|
|
25
|
+
"""Create a new entry. Returns 201 Created."""
|
|
26
|
+
# Convert files and dirs to dicts
|
|
27
|
+
files_data = []
|
|
28
|
+
for f in data.files:
|
|
29
|
+
file_dict = {}
|
|
30
|
+
if f.path is not None:
|
|
31
|
+
file_dict["path"] = f.path
|
|
32
|
+
if f.content is not None:
|
|
33
|
+
file_dict["content"] = f.content
|
|
34
|
+
if f.local_path is not None:
|
|
35
|
+
file_dict["local_path"] = f.local_path
|
|
36
|
+
files_data.append(file_dict)
|
|
37
|
+
|
|
38
|
+
dirs_data = []
|
|
39
|
+
for d in data.dirs:
|
|
40
|
+
dirs_data.append({"path": d.path})
|
|
41
|
+
|
|
42
|
+
return service.create_entry(
|
|
43
|
+
summary=data.summary,
|
|
44
|
+
slug=data.slug,
|
|
45
|
+
tags=data.tags,
|
|
46
|
+
files_data=files_data if files_data else None,
|
|
47
|
+
dirs_data=dirs_data if dirs_data else None,
|
|
48
|
+
expires_in=data.expires_in,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("")
|
|
53
|
+
async def list_entries(
|
|
54
|
+
request: Request,
|
|
55
|
+
q: str | None = Query(None),
|
|
56
|
+
tags: str | None = Query(None),
|
|
57
|
+
status: str | None = Query(None),
|
|
58
|
+
page: int = Query(1, ge=1),
|
|
59
|
+
per_page: int = Query(20, ge=1, le=100),
|
|
60
|
+
service: EntryService = Depends(_get_service),
|
|
61
|
+
):
|
|
62
|
+
"""List entries with search, filter, and pagination."""
|
|
63
|
+
tag_list = tags.split(",") if tags else None
|
|
64
|
+
return service.list_entries(q=q, tags=tag_list, status=status, page=page, per_page=per_page)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.get("/{slug}")
|
|
68
|
+
async def get_entry(
|
|
69
|
+
slug: str,
|
|
70
|
+
request: Request,
|
|
71
|
+
service: EntryService = Depends(_get_service),
|
|
72
|
+
):
|
|
73
|
+
"""Get entry details by slug."""
|
|
74
|
+
return service.get_entry(slug)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.patch("/{slug}")
|
|
78
|
+
async def update_entry(
|
|
79
|
+
slug: str,
|
|
80
|
+
data: EntryUpdate,
|
|
81
|
+
request: Request,
|
|
82
|
+
service: EntryService = Depends(_get_service),
|
|
83
|
+
):
|
|
84
|
+
"""Update an entry."""
|
|
85
|
+
# Convert add_files to dicts
|
|
86
|
+
add_files = None
|
|
87
|
+
if data.add_files:
|
|
88
|
+
add_files = []
|
|
89
|
+
for f in data.add_files:
|
|
90
|
+
file_dict = {}
|
|
91
|
+
if f.path is not None:
|
|
92
|
+
file_dict["path"] = f.path
|
|
93
|
+
if f.content is not None:
|
|
94
|
+
file_dict["content"] = f.content
|
|
95
|
+
if f.local_path is not None:
|
|
96
|
+
file_dict["local_path"] = f.local_path
|
|
97
|
+
add_files.append(file_dict)
|
|
98
|
+
|
|
99
|
+
# Convert add_dirs to dicts
|
|
100
|
+
add_dirs = None
|
|
101
|
+
if data.add_dirs:
|
|
102
|
+
add_dirs = [{"path": d.path} for d in data.add_dirs]
|
|
103
|
+
|
|
104
|
+
return service.update_entry(
|
|
105
|
+
slug=slug,
|
|
106
|
+
summary=data.summary,
|
|
107
|
+
status=data.status,
|
|
108
|
+
tags=data.tags,
|
|
109
|
+
add_files=add_files,
|
|
110
|
+
remove_file_ids=data.remove_file_ids,
|
|
111
|
+
add_dirs=add_dirs,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.delete("/{slug}")
|
|
116
|
+
async def delete_entry(
|
|
117
|
+
slug: str,
|
|
118
|
+
request: Request,
|
|
119
|
+
service: EntryService = Depends(_get_service),
|
|
120
|
+
):
|
|
121
|
+
"""Delete entry by slug."""
|
|
122
|
+
service.delete_entry(slug)
|
|
123
|
+
return {"ok": True}
|
peek/api/files.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""File download and content API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import Response
|
|
9
|
+
from sqlmodel import Session, select
|
|
10
|
+
|
|
11
|
+
from peek.database import get_engine
|
|
12
|
+
from peek.exceptions import NotFoundError
|
|
13
|
+
from peek.models import Entry, File
|
|
14
|
+
from peek.storage import StorageManager
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/v1/entries", tags=["files"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sanitize_filename(filename: str) -> str:
|
|
20
|
+
"""Sanitize filename for Content-Disposition header to prevent injection.
|
|
21
|
+
|
|
22
|
+
Removes quotes, semicolons, and newlines that could break the header.
|
|
23
|
+
"""
|
|
24
|
+
# Remove characters that could inject additional headers
|
|
25
|
+
sanitized = re.sub(r'[";\r\n]', "", filename)
|
|
26
|
+
# Limit length
|
|
27
|
+
if len(sanitized) > 200:
|
|
28
|
+
sanitized = sanitized[:200]
|
|
29
|
+
return sanitized
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _language_to_content_type(language: str | None) -> str:
|
|
33
|
+
"""Map language ID to Content-Type for inline display."""
|
|
34
|
+
_TYPE_MAP = {
|
|
35
|
+
"python": "text/x-python",
|
|
36
|
+
"javascript": "text/javascript",
|
|
37
|
+
"typescript": "text/typescript",
|
|
38
|
+
"html": "text/html",
|
|
39
|
+
"css": "text/css",
|
|
40
|
+
"json": "application/json",
|
|
41
|
+
"yaml": "text/yaml",
|
|
42
|
+
"xml": "text/xml",
|
|
43
|
+
"markdown": "text/markdown",
|
|
44
|
+
"sql": "text/x-sql",
|
|
45
|
+
"bash": "text/x-shellscript",
|
|
46
|
+
"go": "text/x-go",
|
|
47
|
+
"rust": "text/x-rust",
|
|
48
|
+
"java": "text/x-java",
|
|
49
|
+
"cpp": "text/x-c++src",
|
|
50
|
+
"text": "text/plain",
|
|
51
|
+
}
|
|
52
|
+
if language and language in _TYPE_MAP:
|
|
53
|
+
return _TYPE_MAP[language]
|
|
54
|
+
return "text/plain; charset=utf-8"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.get("/{slug}/files/{file_id}")
|
|
58
|
+
async def download_file(slug: str, file_id: int, request: Request):
|
|
59
|
+
"""Download a single file (with Content-Disposition: attachment)."""
|
|
60
|
+
config = request.app.state.config
|
|
61
|
+
engine = get_engine(config)
|
|
62
|
+
storage = StorageManager(config=config)
|
|
63
|
+
|
|
64
|
+
with Session(engine) as session:
|
|
65
|
+
entry = session.exec(select(Entry).where(Entry.slug == slug)).first()
|
|
66
|
+
if not entry:
|
|
67
|
+
raise NotFoundError(f"Entry not found: {slug}")
|
|
68
|
+
|
|
69
|
+
file_record = session.exec(
|
|
70
|
+
select(File).where(File.id == file_id, File.entry_id == entry.id)
|
|
71
|
+
).first()
|
|
72
|
+
if not file_record:
|
|
73
|
+
raise NotFoundError(f"File not found: {file_id}")
|
|
74
|
+
|
|
75
|
+
content = storage.read_file(entry.id, file_record.filename, file_record.path)
|
|
76
|
+
safe_name = _sanitize_filename(file_record.filename)
|
|
77
|
+
return Response(
|
|
78
|
+
content=content,
|
|
79
|
+
media_type="application/octet-stream",
|
|
80
|
+
headers={"Content-Disposition": f'attachment; filename="{safe_name}"'},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("/{slug}/files/{file_id}/content")
|
|
85
|
+
async def get_file_content(slug: str, file_id: int, request: Request):
|
|
86
|
+
"""Get file content inline (raw text, no Content-Disposition).
|
|
87
|
+
|
|
88
|
+
Returns the file content with an appropriate Content-Type based on
|
|
89
|
+
language. No Content-Disposition header — suitable for inline display.
|
|
90
|
+
"""
|
|
91
|
+
config = request.app.state.config
|
|
92
|
+
engine = get_engine(config)
|
|
93
|
+
storage = StorageManager(config=config)
|
|
94
|
+
|
|
95
|
+
with Session(engine) as session:
|
|
96
|
+
entry = session.exec(select(Entry).where(Entry.slug == slug)).first()
|
|
97
|
+
if not entry:
|
|
98
|
+
raise NotFoundError(f"Entry not found: {slug}")
|
|
99
|
+
|
|
100
|
+
file_record = session.exec(
|
|
101
|
+
select(File).where(File.id == file_id, File.entry_id == entry.id)
|
|
102
|
+
).first()
|
|
103
|
+
if not file_record:
|
|
104
|
+
raise NotFoundError(f"File not found: {file_id}")
|
|
105
|
+
|
|
106
|
+
content = storage.read_file(entry.id, file_record.filename, file_record.path)
|
|
107
|
+
|
|
108
|
+
# Determine Content-Type from language
|
|
109
|
+
content_type = _language_to_content_type(file_record.language)
|
|
110
|
+
return Response(
|
|
111
|
+
content=content,
|
|
112
|
+
media_type=content_type,
|
|
113
|
+
# No Content-Disposition — inline display
|
|
114
|
+
)
|
peek/cli.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""CLI commands for Peek.
|
|
2
|
+
|
|
3
|
+
Provides command-line interface for:
|
|
4
|
+
- Starting the server (`peek serve`)
|
|
5
|
+
- Creating entries (`peek create`)
|
|
6
|
+
- Getting entries (`peek get`)
|
|
7
|
+
- Listing entries (`peek list`)
|
|
8
|
+
- Deleting entries (`peek delete`)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from sqlalchemy import select
|
|
18
|
+
from sqlmodel import Session
|
|
19
|
+
|
|
20
|
+
from peek.config import PeekConfig
|
|
21
|
+
from peek.database import init_db
|
|
22
|
+
from peek.main import create_app
|
|
23
|
+
from peek.models import CreateEntryRequest, EntryCreate
|
|
24
|
+
from peek.services.entry_service import EntryService
|
|
25
|
+
from peek.services.file_service import scan_directory
|
|
26
|
+
from peek.storage import StorageManager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
@click.version_option(version="0.1.0", prog_name="peek")
|
|
31
|
+
def cli() -> None:
|
|
32
|
+
"""Peek - A lightweight code & document formatting display service."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
@click.option("--host", "-h", default=None, help="Server bind address (default: 127.0.0.1)")
|
|
38
|
+
@click.option("--port", "-p", default=None, type=int, help="Server port (default: 8080)")
|
|
39
|
+
@click.option("--reload", is_flag=True, help="Enable auto-reload (development)")
|
|
40
|
+
@click.option("--workers", "-w", default=1, type=int, help="Number of worker processes")
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def serve(ctx: click.Context, host: str | None, port: int | None, reload: bool, workers: int) -> None:
|
|
43
|
+
"""Start the Peek server.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
peek serve # Start with default config
|
|
47
|
+
peek serve -p 3000 # Start on port 3000
|
|
48
|
+
peek serve --reload # Development mode with auto-reload
|
|
49
|
+
"""
|
|
50
|
+
import uvicorn
|
|
51
|
+
|
|
52
|
+
config = PeekConfig()
|
|
53
|
+
|
|
54
|
+
# Override with CLI args
|
|
55
|
+
bind_host = host or config.server.host
|
|
56
|
+
bind_port = port or config.server.port
|
|
57
|
+
|
|
58
|
+
# Ensure data directory exists
|
|
59
|
+
config.ensure_directories()
|
|
60
|
+
|
|
61
|
+
# Initialize database
|
|
62
|
+
init_db(config.db_path)
|
|
63
|
+
|
|
64
|
+
click.echo(f"Starting Peek server on http://{bind_host}:{bind_port}")
|
|
65
|
+
click.echo(f"Data directory: {config.data_dir}")
|
|
66
|
+
click.echo(f"Database: {config.db_path}")
|
|
67
|
+
|
|
68
|
+
uvicorn.run(
|
|
69
|
+
"peek.main:get_app",
|
|
70
|
+
host=bind_host,
|
|
71
|
+
port=bind_port,
|
|
72
|
+
reload=reload,
|
|
73
|
+
workers=1 if reload else workers,
|
|
74
|
+
factory=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@cli.command()
|
|
79
|
+
@click.argument("paths", nargs=-1, required=False)
|
|
80
|
+
@click.option("--summary", "-s", required=True, help="Entry summary/description")
|
|
81
|
+
@click.option("--slug", help="Custom URL slug (auto-generated if not provided)")
|
|
82
|
+
@click.option("--tag", "-t", multiple=True, help="Tags (can be specified multiple times)")
|
|
83
|
+
@click.option("--expires-in", help="Expiration duration (e.g., '7d', '1h', '30m')")
|
|
84
|
+
@click.option("--from-stdin", is_flag=True, help="Read file content from stdin")
|
|
85
|
+
@click.option("--json-output", "-j", is_flag=True, help="Output as JSON")
|
|
86
|
+
def create(
|
|
87
|
+
paths: tuple[str, ...],
|
|
88
|
+
summary: str,
|
|
89
|
+
slug: str | None,
|
|
90
|
+
tag: tuple[str, ...],
|
|
91
|
+
expires_in: str | None,
|
|
92
|
+
from_stdin: bool,
|
|
93
|
+
json_output: bool,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Create a new entry.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
peek create file.txt -s "My code"
|
|
99
|
+
peek create src/*.py -s "Python project" -t python -t cli
|
|
100
|
+
peek create -s "From stdin" --from-stdin < code.py
|
|
101
|
+
echo "content" | peek create -s "From pipe" --from-stdin
|
|
102
|
+
"""
|
|
103
|
+
config = PeekConfig()
|
|
104
|
+
config.ensure_directories()
|
|
105
|
+
|
|
106
|
+
engine = init_db(config.db_path)
|
|
107
|
+
storage = StorageManager(config=config)
|
|
108
|
+
service = EntryService(engine=engine, storage=storage, config=config)
|
|
109
|
+
|
|
110
|
+
# Collect files
|
|
111
|
+
files_data = []
|
|
112
|
+
dirs_data = []
|
|
113
|
+
|
|
114
|
+
if from_stdin:
|
|
115
|
+
# Read from stdin
|
|
116
|
+
content = sys.stdin.read()
|
|
117
|
+
filename = "stdin.txt"
|
|
118
|
+
files_data.append({
|
|
119
|
+
"path": filename,
|
|
120
|
+
"filename": filename,
|
|
121
|
+
"content": content,
|
|
122
|
+
})
|
|
123
|
+
elif paths:
|
|
124
|
+
# Process each path
|
|
125
|
+
for path_str in paths:
|
|
126
|
+
path = Path(path_str)
|
|
127
|
+
|
|
128
|
+
if not path.exists():
|
|
129
|
+
click.echo(f"Error: Path not found: {path_str}", err=True)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
if path.is_dir():
|
|
133
|
+
# Add as directory
|
|
134
|
+
dirs_data.append({"path": str(path.resolve())})
|
|
135
|
+
elif path.is_file():
|
|
136
|
+
# Read file content
|
|
137
|
+
try:
|
|
138
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
139
|
+
files_data.append({
|
|
140
|
+
"path": path.name,
|
|
141
|
+
"filename": path.name,
|
|
142
|
+
"content": content,
|
|
143
|
+
})
|
|
144
|
+
except Exception as e:
|
|
145
|
+
click.echo(f"Error reading {path_str}: {e}", err=True)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
else:
|
|
148
|
+
# No paths provided - create empty entry
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = service.create_entry(
|
|
153
|
+
summary=summary,
|
|
154
|
+
slug=slug,
|
|
155
|
+
tags=list(tag),
|
|
156
|
+
files_data=files_data if files_data else None,
|
|
157
|
+
dirs_data=dirs_data if dirs_data else None,
|
|
158
|
+
expires_in=expires_in,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if json_output:
|
|
162
|
+
click.echo(json.dumps({
|
|
163
|
+
"id": result.id,
|
|
164
|
+
"slug": result.slug,
|
|
165
|
+
"url": result.url,
|
|
166
|
+
"created_at": result.created_at.isoformat() if result.created_at else None,
|
|
167
|
+
"file_count": len(result.files),
|
|
168
|
+
}, indent=2))
|
|
169
|
+
else:
|
|
170
|
+
click.echo(f"✓ Created entry: {result.slug}")
|
|
171
|
+
click.echo(f" URL: {result.url}")
|
|
172
|
+
click.echo(f" Files: {len(result.files)}")
|
|
173
|
+
if result.files:
|
|
174
|
+
for f in result.files:
|
|
175
|
+
click.echo(f" - {f.path or f.filename}")
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
click.echo(f"Error: {e}", err=True)
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@cli.command()
|
|
183
|
+
@click.argument("slug")
|
|
184
|
+
@click.option("--json-output", "-j", is_flag=True, help="Output as JSON")
|
|
185
|
+
def get(slug: str, json_output: bool) -> None:
|
|
186
|
+
"""Get entry details by slug.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
peek get my-entry
|
|
190
|
+
peek get my-entry --json
|
|
191
|
+
"""
|
|
192
|
+
config = PeekConfig()
|
|
193
|
+
engine = init_db(config.db_path)
|
|
194
|
+
storage = StorageManager(config=config)
|
|
195
|
+
service = EntryService(engine=engine, storage=storage, config=config)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
entry = service.get_entry(slug)
|
|
199
|
+
|
|
200
|
+
if json_output:
|
|
201
|
+
click.echo(json.dumps({
|
|
202
|
+
"id": entry.id,
|
|
203
|
+
"slug": entry.slug,
|
|
204
|
+
"summary": entry.summary,
|
|
205
|
+
"status": entry.status,
|
|
206
|
+
"tags": entry.tags,
|
|
207
|
+
"files": [
|
|
208
|
+
{
|
|
209
|
+
"id": f.id,
|
|
210
|
+
"path": f.path,
|
|
211
|
+
"filename": f.filename,
|
|
212
|
+
"language": f.language,
|
|
213
|
+
"size": f.size,
|
|
214
|
+
}
|
|
215
|
+
for f in entry.files
|
|
216
|
+
],
|
|
217
|
+
"created_at": entry.created_at.isoformat() if entry.created_at else None,
|
|
218
|
+
"updated_at": entry.updated_at.isoformat() if entry.updated_at else None,
|
|
219
|
+
}, indent=2))
|
|
220
|
+
else:
|
|
221
|
+
click.echo(f"Entry: {entry.slug}")
|
|
222
|
+
click.echo(f"Summary: {entry.summary}")
|
|
223
|
+
click.echo(f"Status: {entry.status}")
|
|
224
|
+
if entry.tags:
|
|
225
|
+
click.echo(f"Tags: {', '.join(entry.tags)}")
|
|
226
|
+
click.echo(f"Files: {len(entry.files)}")
|
|
227
|
+
for f in entry.files:
|
|
228
|
+
lang_info = f" ({f.language})" if f.language else ""
|
|
229
|
+
click.echo(f" - {f.path or f.filename}{lang_info} - {f.size} bytes")
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
click.echo(f"Error: {e}", err=True)
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@cli.command(name="list")
|
|
237
|
+
@click.option("--query", "-q", help="Search query (FTS5 search)")
|
|
238
|
+
@click.option("--tag", "-t", multiple=True, help="Filter by tag")
|
|
239
|
+
@click.option("--status", "-s", help="Filter by status")
|
|
240
|
+
@click.option("--page", "-p", default=1, type=int, help="Page number")
|
|
241
|
+
@click.option("--per-page", default=20, type=int, help="Items per page")
|
|
242
|
+
@click.option("--json-output", "-j", is_flag=True, help="Output as JSON")
|
|
243
|
+
def list_entries(
|
|
244
|
+
query: str | None,
|
|
245
|
+
tag: tuple[str, ...],
|
|
246
|
+
status: str | None,
|
|
247
|
+
page: int,
|
|
248
|
+
per_page: int,
|
|
249
|
+
json_output: bool,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""List entries with optional filters.
|
|
252
|
+
|
|
253
|
+
Examples:
|
|
254
|
+
peek list
|
|
255
|
+
peek list -q "python"
|
|
256
|
+
peek list -t cli -t python
|
|
257
|
+
peek list --status active
|
|
258
|
+
"""
|
|
259
|
+
config = PeekConfig()
|
|
260
|
+
engine = init_db(config.db_path)
|
|
261
|
+
storage = StorageManager(config=config)
|
|
262
|
+
service = EntryService(engine=engine, storage=storage, config=config)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
tag_list = list(tag) if tag else None
|
|
266
|
+
result = service.list_entries(
|
|
267
|
+
q=query,
|
|
268
|
+
tags=tag_list,
|
|
269
|
+
status=status,
|
|
270
|
+
page=page,
|
|
271
|
+
per_page=per_page,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if json_output:
|
|
275
|
+
click.echo(json.dumps({
|
|
276
|
+
"items": [
|
|
277
|
+
{
|
|
278
|
+
"id": item.id,
|
|
279
|
+
"slug": item.slug,
|
|
280
|
+
"summary": item.summary,
|
|
281
|
+
"tags": item.tags,
|
|
282
|
+
"status": item.status,
|
|
283
|
+
"file_count": item.file_count,
|
|
284
|
+
"created_at": item.created_at.isoformat() if item.created_at else None,
|
|
285
|
+
"updated_at": item.updated_at.isoformat() if item.updated_at else None,
|
|
286
|
+
}
|
|
287
|
+
for item in result.items
|
|
288
|
+
],
|
|
289
|
+
"total": result.total,
|
|
290
|
+
"page": result.page,
|
|
291
|
+
"per_page": result.per_page,
|
|
292
|
+
}, indent=2))
|
|
293
|
+
else:
|
|
294
|
+
click.echo(f"Entries ({result.total} total, page {result.page}):")
|
|
295
|
+
click.echo()
|
|
296
|
+
for item in result.items:
|
|
297
|
+
tags_str = f" [{', '.join(item.tags)}]" if item.tags else ""
|
|
298
|
+
click.echo(f" {item.slug}{tags_str}")
|
|
299
|
+
click.echo(f" {item.summary[:60]}{'...' if len(item.summary) > 60 else ''}")
|
|
300
|
+
click.echo(f" {item.file_count} files | {item.status}")
|
|
301
|
+
click.echo()
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
click.echo(f"Error: {e}", err=True)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@cli.command()
|
|
309
|
+
@click.argument("slug")
|
|
310
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this entry?")
|
|
311
|
+
def delete(slug: str) -> None:
|
|
312
|
+
"""Delete an entry by slug.
|
|
313
|
+
|
|
314
|
+
Examples:
|
|
315
|
+
peek delete my-entry
|
|
316
|
+
peek delete my-entry --yes # Skip confirmation
|
|
317
|
+
"""
|
|
318
|
+
config = PeekConfig()
|
|
319
|
+
engine = init_db(config.db_path)
|
|
320
|
+
storage = StorageManager(config=config)
|
|
321
|
+
service = EntryService(engine=engine, storage=storage, config=config)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
service.delete_entry(slug)
|
|
325
|
+
click.echo(f"✓ Deleted entry: {slug}")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
click.echo(f"Error: {e}", err=True)
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
cli()
|