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.
Files changed (302) hide show
  1. peek/__init__.py +3 -0
  2. peek/__main__.py +6 -0
  3. peek/api/__init__.py +1 -0
  4. peek/api/entries.py +123 -0
  5. peek/api/files.py +114 -0
  6. peek/cli.py +332 -0
  7. peek/config.py +232 -0
  8. peek/database.py +304 -0
  9. peek/exceptions.py +127 -0
  10. peek/language.py +407 -0
  11. peek/main.py +224 -0
  12. peek/models.py +330 -0
  13. peek/services/__init__.py +1 -0
  14. peek/services/entry_service.py +612 -0
  15. peek/services/file_service.py +231 -0
  16. peek/static/assets/EntryDetailView-Cnhdjikj.css +1 -0
  17. peek/static/assets/EntryDetailView-yI3ydvJn.js +77 -0
  18. peek/static/assets/EntryListView-CjYcs8e1.js +1 -0
  19. peek/static/assets/EntryListView-DjPX64rQ.css +1 -0
  20. peek/static/assets/abap-DsBKuouk.js +1 -0
  21. peek/static/assets/actionscript-3-D_z4Izcz.js +1 -0
  22. peek/static/assets/ada-727ZlQH0.js +1 -0
  23. peek/static/assets/andromeeda-C3khCPGq.js +1 -0
  24. peek/static/assets/angular-html-LfdN0zeE.js +1 -0
  25. peek/static/assets/angular-ts-CKsD7JZE.js +1 -0
  26. peek/static/assets/apache-Dn00JSTd.js +1 -0
  27. peek/static/assets/apex-COJ4H7py.js +1 -0
  28. peek/static/assets/apl-BBq3IX1j.js +1 -0
  29. peek/static/assets/applescript-Bu5BbsvL.js +1 -0
  30. peek/static/assets/ara-7O62HKoU.js +1 -0
  31. peek/static/assets/asciidoc-BPT9niGB.js +1 -0
  32. peek/static/assets/asm-Dhn9LcZ4.js +1 -0
  33. peek/static/assets/astro-CqkE3fuf.js +1 -0
  34. peek/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  35. peek/static/assets/awk-eg146-Ew.js +1 -0
  36. peek/static/assets/ayu-dark-Cv9koXgw.js +1 -0
  37. peek/static/assets/ballerina-Du268qiB.js +1 -0
  38. peek/static/assets/bat-fje9CFhw.js +1 -0
  39. peek/static/assets/beancount-BwXTMy5W.js +1 -0
  40. peek/static/assets/berry-3xVqZejG.js +1 -0
  41. peek/static/assets/bibtex-xW4inM5L.js +1 -0
  42. peek/static/assets/bicep-DHo0CJ0O.js +1 -0
  43. peek/static/assets/blade-a8OxSdnT.js +1 -0
  44. peek/static/assets/bsl-Dgyn0ogV.js +1 -0
  45. peek/static/assets/c-C3t2pwGQ.js +1 -0
  46. peek/static/assets/cadence-DNquZEk8.js +1 -0
  47. peek/static/assets/cairo--RitsXJZ.js +1 -0
  48. peek/static/assets/catppuccin-frappe-CD_QflpE.js +1 -0
  49. peek/static/assets/catppuccin-latte-DRW-0cLl.js +1 -0
  50. peek/static/assets/catppuccin-macchiato-C-_shW-Y.js +1 -0
  51. peek/static/assets/catppuccin-mocha-LGGdnPYs.js +1 -0
  52. peek/static/assets/clarity-BHOwM8T6.js +1 -0
  53. peek/static/assets/clojure-DxSadP1t.js +1 -0
  54. peek/static/assets/cmake-DbXoA79R.js +1 -0
  55. peek/static/assets/cobol-PTqiYgYu.js +1 -0
  56. peek/static/assets/codeowners-Bp6g37R7.js +1 -0
  57. peek/static/assets/codeql-sacFqUAJ.js +1 -0
  58. peek/static/assets/coffee-dyiR41kL.js +1 -0
  59. peek/static/assets/common-lisp-C7gG9l05.js +1 -0
  60. peek/static/assets/coq-Dsg_Bt_b.js +1 -0
  61. peek/static/assets/cpp-BksuvNSY.js +1 -0
  62. peek/static/assets/crystal-DtDmRg-F.js +1 -0
  63. peek/static/assets/csharp-D9R-vmeu.js +1 -0
  64. peek/static/assets/css-BPhBrDlE.js +1 -0
  65. peek/static/assets/csv-B0qRVHPH.js +1 -0
  66. peek/static/assets/cue-DtFQj3wx.js +1 -0
  67. peek/static/assets/cypher-m2LEI-9-.js +1 -0
  68. peek/static/assets/d-BoXegm-a.js +1 -0
  69. peek/static/assets/dark-plus-C3mMm8J8.js +1 -0
  70. peek/static/assets/dart-B9wLZaAG.js +1 -0
  71. peek/static/assets/dax-ClGRhx96.js +1 -0
  72. peek/static/assets/desktop-DEIpsLCJ.js +1 -0
  73. peek/static/assets/diff-BgYniUM_.js +1 -0
  74. peek/static/assets/docker-COcR7UxN.js +1 -0
  75. peek/static/assets/dotenv-BjQB5zDj.js +1 -0
  76. peek/static/assets/dracula-BzJJZx-M.js +1 -0
  77. peek/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  78. peek/static/assets/dream-maker-C-nORZOA.js +1 -0
  79. peek/static/assets/edge-D5gP-w-T.js +1 -0
  80. peek/static/assets/elixir-CLiX3zqd.js +1 -0
  81. peek/static/assets/elm-CmHSxxaM.js +1 -0
  82. peek/static/assets/emacs-lisp-BX77sIaO.js +1 -0
  83. peek/static/assets/erb-BYTLMnw6.js +1 -0
  84. peek/static/assets/erlang-B-DoSBHF.js +1 -0
  85. peek/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  86. peek/static/assets/everforest-light-C8M2exoo.js +1 -0
  87. peek/static/assets/fennel-bCA53EVm.js +1 -0
  88. peek/static/assets/fish-w-ucz2PV.js +1 -0
  89. peek/static/assets/fluent-Dayu4EKP.js +1 -0
  90. peek/static/assets/fortran-fixed-form-TqA4NnZg.js +1 -0
  91. peek/static/assets/fortran-free-form-DKXYxT9g.js +1 -0
  92. peek/static/assets/fsharp-XplgxFYe.js +1 -0
  93. peek/static/assets/gdresource-BHYsBjWJ.js +1 -0
  94. peek/static/assets/gdscript-DfxzS6Rs.js +1 -0
  95. peek/static/assets/gdshader-SKMF96pI.js +1 -0
  96. peek/static/assets/genie-ajMbGru0.js +1 -0
  97. peek/static/assets/gherkin--30QC5Em.js +1 -0
  98. peek/static/assets/git-commit-i4q6IMui.js +1 -0
  99. peek/static/assets/git-rebase-B-v9cOL2.js +1 -0
  100. peek/static/assets/github-dark-DHJKELXO.js +1 -0
  101. peek/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  102. peek/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  103. peek/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  104. peek/static/assets/github-light-DAi9KRSo.js +1 -0
  105. peek/static/assets/github-light-default-D7oLnXFd.js +1 -0
  106. peek/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  107. peek/static/assets/gleam-B430Bg39.js +1 -0
  108. peek/static/assets/glimmer-js-D-cwc0-E.js +1 -0
  109. peek/static/assets/glimmer-ts-pgjy16dm.js +1 -0
  110. peek/static/assets/glsl-DBO2IWDn.js +1 -0
  111. peek/static/assets/gnuplot-CM8KxXT1.js +1 -0
  112. peek/static/assets/go-B1SYOhNW.js +1 -0
  113. peek/static/assets/graphql-cDcHW_If.js +1 -0
  114. peek/static/assets/groovy-DkBy-JyN.js +1 -0
  115. peek/static/assets/hack-D1yCygmZ.js +1 -0
  116. peek/static/assets/haml-B2EZWmdv.js +1 -0
  117. peek/static/assets/handlebars-BQGss363.js +1 -0
  118. peek/static/assets/haskell-BILxekzW.js +1 -0
  119. peek/static/assets/haxe-C5wWYbrZ.js +1 -0
  120. peek/static/assets/hcl-HzYwdGDm.js +1 -0
  121. peek/static/assets/hjson-T-Tgc4AT.js +1 -0
  122. peek/static/assets/hlsl-ifBTmRxC.js +1 -0
  123. peek/static/assets/houston-DnULxvSX.js +1 -0
  124. peek/static/assets/html-C2L_23MC.js +1 -0
  125. peek/static/assets/html-derivative-CSfWNPLT.js +1 -0
  126. peek/static/assets/http-FRrOvY1W.js +1 -0
  127. peek/static/assets/hxml-TIA70rKU.js +1 -0
  128. peek/static/assets/hy-BMj5Y0dO.js +1 -0
  129. peek/static/assets/imba-bv_oIlVt.js +1 -0
  130. peek/static/assets/index-CMhcFTfH.js +26 -0
  131. peek/static/assets/index-DvcXrXhI.css +1 -0
  132. peek/static/assets/ini-BjABl1g7.js +1 -0
  133. peek/static/assets/java-xI-RfyKK.js +1 -0
  134. peek/static/assets/javascript-ySlJ1b_l.js +1 -0
  135. peek/static/assets/jinja-DGy0s7-h.js +1 -0
  136. peek/static/assets/jison-BqZprYcd.js +1 -0
  137. peek/static/assets/json-BQoSv7ci.js +1 -0
  138. peek/static/assets/json5-w8dY5SsB.js +1 -0
  139. peek/static/assets/jsonc-TU54ms6u.js +1 -0
  140. peek/static/assets/jsonl-DREVFZK8.js +1 -0
  141. peek/static/assets/jsonnet-BfivnA6A.js +1 -0
  142. peek/static/assets/jssm-P4WzXJd0.js +1 -0
  143. peek/static/assets/jsx-BAng5TT0.js +1 -0
  144. peek/static/assets/julia-BBuGR-5E.js +1 -0
  145. peek/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  146. peek/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  147. peek/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  148. peek/static/assets/kotlin-B5lbUyaz.js +1 -0
  149. peek/static/assets/kusto-mebxcVVE.js +1 -0
  150. peek/static/assets/laserwave-DUszq2jm.js +1 -0
  151. peek/static/assets/latex-C-cWTeAZ.js +1 -0
  152. peek/static/assets/lean-XBlWyCtg.js +1 -0
  153. peek/static/assets/less-BfCpw3nA.js +1 -0
  154. peek/static/assets/light-plus-B7mTdjB0.js +1 -0
  155. peek/static/assets/liquid-D3W5UaiH.js +1 -0
  156. peek/static/assets/log-Cc5clBb7.js +1 -0
  157. peek/static/assets/logo-IuBKFhSY.js +1 -0
  158. peek/static/assets/lua-CvWAzNxB.js +1 -0
  159. peek/static/assets/luau-Du5NY7AG.js +1 -0
  160. peek/static/assets/make-Bvotw-X0.js +1 -0
  161. peek/static/assets/markdown-UIAJJxZW.js +1 -0
  162. peek/static/assets/marko-z0MBrx5-.js +1 -0
  163. peek/static/assets/material-theme-D5KoaKCx.js +1 -0
  164. peek/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  165. peek/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  166. peek/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  167. peek/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  168. peek/static/assets/matlab-D9-PGadD.js +1 -0
  169. peek/static/assets/mdc-DB_EDNY_.js +1 -0
  170. peek/static/assets/mdx-sdHcTMYB.js +1 -0
  171. peek/static/assets/mermaid-Ci6OQyBP.js +1 -0
  172. peek/static/assets/min-dark-CafNBF8u.js +1 -0
  173. peek/static/assets/min-light-CTRr51gU.js +1 -0
  174. peek/static/assets/mipsasm-BC5c_5Pe.js +1 -0
  175. peek/static/assets/mojo-Tz6hzZYG.js +1 -0
  176. peek/static/assets/monokai-D4h5O-jR.js +1 -0
  177. peek/static/assets/move-DB_GagMm.js +1 -0
  178. peek/static/assets/narrat-DLbgOhZU.js +1 -0
  179. peek/static/assets/nextflow-B0XVJmRM.js +1 -0
  180. peek/static/assets/nginx-D_VnBJ67.js +1 -0
  181. peek/static/assets/night-owl-C39BiMTA.js +1 -0
  182. peek/static/assets/nim-ZlGxZxc3.js +1 -0
  183. peek/static/assets/nix-shcSOmrb.js +1 -0
  184. peek/static/assets/nord-Ddv68eIx.js +1 -0
  185. peek/static/assets/nushell-D4Tzg5kh.js +1 -0
  186. peek/static/assets/objective-c-Deuh7S70.js +1 -0
  187. peek/static/assets/objective-cpp-BUEGK8hf.js +1 -0
  188. peek/static/assets/ocaml-BNioltXt.js +1 -0
  189. peek/static/assets/one-dark-pro-GBQ2dnAY.js +1 -0
  190. peek/static/assets/one-light-PoHY5YXO.js +1 -0
  191. peek/static/assets/pascal-JqZropPD.js +1 -0
  192. peek/static/assets/perl-CHQXSrWU.js +1 -0
  193. peek/static/assets/php-B5ebYQev.js +1 -0
  194. peek/static/assets/plastic-3e1v2bzS.js +1 -0
  195. peek/static/assets/plsql-LKU2TuZ1.js +1 -0
  196. peek/static/assets/po-BFLt1xDp.js +1 -0
  197. peek/static/assets/poimandres-CS3Unz2-.js +1 -0
  198. peek/static/assets/polar-DKykz6zU.js +1 -0
  199. peek/static/assets/postcss-B3ZDOciz.js +1 -0
  200. peek/static/assets/powerquery-CSHBycmS.js +1 -0
  201. peek/static/assets/powershell-BIEUsx6d.js +1 -0
  202. peek/static/assets/prisma-B48N-Iqd.js +1 -0
  203. peek/static/assets/prolog-BY-TUvya.js +1 -0
  204. peek/static/assets/proto-zocC4JxJ.js +1 -0
  205. peek/static/assets/pug-CM9l7STV.js +1 -0
  206. peek/static/assets/puppet-Cza_XSSt.js +1 -0
  207. peek/static/assets/purescript-Bg-kzb6g.js +1 -0
  208. peek/static/assets/python-DhUJRlN_.js +1 -0
  209. peek/static/assets/qml-D8XfuvdV.js +1 -0
  210. peek/static/assets/qmldir-C8lEn-DE.js +1 -0
  211. peek/static/assets/qss-DhMKtDLN.js +1 -0
  212. peek/static/assets/r-CwjWoCRV.js +1 -0
  213. peek/static/assets/racket-CzouJOBO.js +1 -0
  214. peek/static/assets/raku-B1bQXN8T.js +1 -0
  215. peek/static/assets/razor-CNLDkMZG.js +1 -0
  216. peek/static/assets/red-bN70gL4F.js +1 -0
  217. peek/static/assets/reg-5LuOXUq_.js +1 -0
  218. peek/static/assets/regexp-DWJ3fJO_.js +1 -0
  219. peek/static/assets/rel-DJlmqQ1C.js +1 -0
  220. peek/static/assets/riscv-QhoSD0DR.js +1 -0
  221. peek/static/assets/rose-pine-CmCqftbK.js +1 -0
  222. peek/static/assets/rose-pine-dawn-Ds-gbosJ.js +1 -0
  223. peek/static/assets/rose-pine-moon-CjDtw9vr.js +1 -0
  224. peek/static/assets/rst-4NLicBqY.js +1 -0
  225. peek/static/assets/ruby-DeZ3UC14.js +1 -0
  226. peek/static/assets/rust-Be6lgOlo.js +1 -0
  227. peek/static/assets/sas-BmTFh92c.js +1 -0
  228. peek/static/assets/sass-BJ4Li9vH.js +1 -0
  229. peek/static/assets/scala-DQVVAn-B.js +1 -0
  230. peek/static/assets/scheme-BJGe-b2p.js +1 -0
  231. peek/static/assets/scss-C31hgJw-.js +1 -0
  232. peek/static/assets/sdbl-BLhTXw86.js +1 -0
  233. peek/static/assets/shaderlab-B7qAK45m.js +1 -0
  234. peek/static/assets/shellscript-atvbtKCR.js +1 -0
  235. peek/static/assets/shellsession-C_rIy8kc.js +1 -0
  236. peek/static/assets/slack-dark-BthQWCQV.js +1 -0
  237. peek/static/assets/slack-ochin-DqwNpetd.js +1 -0
  238. peek/static/assets/smalltalk-DkLiglaE.js +1 -0
  239. peek/static/assets/snazzy-light-Bw305WKR.js +1 -0
  240. peek/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  241. peek/static/assets/solarized-light-L9t79GZl.js +1 -0
  242. peek/static/assets/solidity-C1w2a3ep.js +1 -0
  243. peek/static/assets/soy-C-lX7w71.js +1 -0
  244. peek/static/assets/sparql-bYkjHRlG.js +1 -0
  245. peek/static/assets/splunk-Cf8iN4DR.js +1 -0
  246. peek/static/assets/sql-COK4E0Yg.js +1 -0
  247. peek/static/assets/ssh-config-BknIz3MU.js +1 -0
  248. peek/static/assets/stata-DorPZHa4.js +1 -0
  249. peek/static/assets/stylus-BeQkCIfX.js +1 -0
  250. peek/static/assets/svelte-MSaWC3Je.js +1 -0
  251. peek/static/assets/swift-BSxZ-RaX.js +1 -0
  252. peek/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  253. peek/static/assets/system-verilog-C7L56vO4.js +1 -0
  254. peek/static/assets/systemd-CUnW07Te.js +1 -0
  255. peek/static/assets/talonscript-C1XDQQGZ.js +1 -0
  256. peek/static/assets/tasl-CQjiPCtT.js +1 -0
  257. peek/static/assets/tcl-DQ1-QYvQ.js +1 -0
  258. peek/static/assets/templ-dwX3ZSMB.js +1 -0
  259. peek/static/assets/terraform-BbSNqyBO.js +1 -0
  260. peek/static/assets/tex-rYs2v40G.js +1 -0
  261. peek/static/assets/tokyo-night-DBQeEorK.js +1 -0
  262. peek/static/assets/toml-CB2ApiWb.js +1 -0
  263. peek/static/assets/ts-tags-CipyTH0X.js +1 -0
  264. peek/static/assets/tsv-B_m7g4N7.js +1 -0
  265. peek/static/assets/tsx-B6W0miNI.js +1 -0
  266. peek/static/assets/turtle-BMR_PYu6.js +1 -0
  267. peek/static/assets/twig-NC5TFiHP.js +1 -0
  268. peek/static/assets/typescript-Dj6nwHGl.js +1 -0
  269. peek/static/assets/typespec-BpWG_bgh.js +1 -0
  270. peek/static/assets/typst-BVUVsWT6.js +1 -0
  271. peek/static/assets/useEntry-DSu-PUHy.js +1 -0
  272. peek/static/assets/useEntry-Dgrs0_hj.css +1 -0
  273. peek/static/assets/v-CAQ2eGtk.js +1 -0
  274. peek/static/assets/vala-BFOHcciG.js +1 -0
  275. peek/static/assets/vb-CdO5JTpU.js +1 -0
  276. peek/static/assets/verilog-CJaU5se_.js +1 -0
  277. peek/static/assets/vesper-BEBZ7ncR.js +1 -0
  278. peek/static/assets/vhdl-DYoNaHQp.js +1 -0
  279. peek/static/assets/viml-m4uW47V2.js +1 -0
  280. peek/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  281. peek/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  282. peek/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  283. peek/static/assets/vue-BuYVFjOK.js +1 -0
  284. peek/static/assets/vue-html-xdeiXROB.js +1 -0
  285. peek/static/assets/vyper-nyqBNV6O.js +1 -0
  286. peek/static/assets/wasm-C6j12Q_x.js +1 -0
  287. peek/static/assets/wasm-CG6Dc4jp.js +1 -0
  288. peek/static/assets/wenyan-7A4Fjokl.js +1 -0
  289. peek/static/assets/wgsl-CB0Krxn9.js +1 -0
  290. peek/static/assets/wikitext-DCE3LsBG.js +1 -0
  291. peek/static/assets/wolfram-C3FkfJm5.js +1 -0
  292. peek/static/assets/xml-e3z08dGr.js +1 -0
  293. peek/static/assets/xsl-Dd0NUgwM.js +1 -0
  294. peek/static/assets/yaml-CVw76BM1.js +1 -0
  295. peek/static/assets/zenscript-HnGAYVZD.js +1 -0
  296. peek/static/assets/zig-BVz_zdnA.js +1 -0
  297. peek/static/index.html +26 -0
  298. peek/storage.py +498 -0
  299. peekview-0.1.0.dist-info/METADATA +171 -0
  300. peekview-0.1.0.dist-info/RECORD +302 -0
  301. peekview-0.1.0.dist-info/WHEEL +4 -0
  302. peekview-0.1.0.dist-info/entry_points.txt +2 -0
peek/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Peek - A lightweight code & document formatting display service."""
2
+
3
+ __version__ = "0.1.0"
peek/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m peek."""
2
+
3
+ from peek.cli import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
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()