workflow.py 90 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820
  1. # encoding: utf-8
  2. #
  3. # Copyright (c) 2014 Dean Jackson <[email protected]>
  4. #
  5. # MIT Licence. See http://opensource.org/licenses/MIT
  6. #
  7. # Created on 2014-02-15
  8. #
  9. """The :class:`Workflow` object is the main interface to this library.
  10. :class:`Workflow` is targeted at Alfred 2. Use
  11. :class:`~workflow.Workflow3` if you want to use Alfred 3's new
  12. features, such as :ref:`workflow variables <workflow-variables>` or
  13. more powerful modifiers.
  14. See :ref:`setup` in the :ref:`user-manual` for an example of how to set
  15. up your Python script to best utilise the :class:`Workflow` object.
  16. """
  17. from __future__ import print_function, unicode_literals
  18. import binascii
  19. import cPickle
  20. from copy import deepcopy
  21. import json
  22. import logging
  23. import logging.handlers
  24. import os
  25. import pickle
  26. import plistlib
  27. import re
  28. import shutil
  29. import string
  30. import subprocess
  31. import sys
  32. import time
  33. import unicodedata
  34. try:
  35. import xml.etree.cElementTree as ET
  36. except ImportError: # pragma: no cover
  37. import xml.etree.ElementTree as ET
  38. # imported to maintain API
  39. from util import AcquisitionError # noqa: F401
  40. from util import (
  41. atomic_writer,
  42. LockFile,
  43. uninterruptible,
  44. )
  45. #: Sentinel for properties that haven't been set yet (that might
  46. #: correctly have the value ``None``)
  47. UNSET = object()
  48. ####################################################################
  49. # Standard system icons
  50. ####################################################################
  51. # These icons are default macOS icons. They are super-high quality, and
  52. # will be familiar to users.
  53. # This library uses `ICON_ERROR` when a workflow dies in flames, so
  54. # in my own workflows, I use `ICON_WARNING` for less fatal errors
  55. # (e.g. bad user input, no results etc.)
  56. # The system icons are all in this directory. There are many more than
  57. # are listed here
  58. ICON_ROOT = '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources'
  59. ICON_ACCOUNT = os.path.join(ICON_ROOT, 'Accounts.icns')
  60. ICON_BURN = os.path.join(ICON_ROOT, 'BurningIcon.icns')
  61. ICON_CLOCK = os.path.join(ICON_ROOT, 'Clock.icns')
  62. ICON_COLOR = os.path.join(ICON_ROOT, 'ProfileBackgroundColor.icns')
  63. ICON_COLOUR = ICON_COLOR # Queen's English, if you please
  64. ICON_EJECT = os.path.join(ICON_ROOT, 'EjectMediaIcon.icns')
  65. # Shown when a workflow throws an error
  66. ICON_ERROR = os.path.join(ICON_ROOT, 'AlertStopIcon.icns')
  67. ICON_FAVORITE = os.path.join(ICON_ROOT, 'ToolbarFavoritesIcon.icns')
  68. ICON_FAVOURITE = ICON_FAVORITE
  69. ICON_GROUP = os.path.join(ICON_ROOT, 'GroupIcon.icns')
  70. ICON_HELP = os.path.join(ICON_ROOT, 'HelpIcon.icns')
  71. ICON_HOME = os.path.join(ICON_ROOT, 'HomeFolderIcon.icns')
  72. ICON_INFO = os.path.join(ICON_ROOT, 'ToolbarInfo.icns')
  73. ICON_NETWORK = os.path.join(ICON_ROOT, 'GenericNetworkIcon.icns')
  74. ICON_NOTE = os.path.join(ICON_ROOT, 'AlertNoteIcon.icns')
  75. ICON_SETTINGS = os.path.join(ICON_ROOT, 'ToolbarAdvanced.icns')
  76. ICON_SWIRL = os.path.join(ICON_ROOT, 'ErasingIcon.icns')
  77. ICON_SWITCH = os.path.join(ICON_ROOT, 'General.icns')
  78. ICON_SYNC = os.path.join(ICON_ROOT, 'Sync.icns')
  79. ICON_TRASH = os.path.join(ICON_ROOT, 'TrashIcon.icns')
  80. ICON_USER = os.path.join(ICON_ROOT, 'UserIcon.icns')
  81. ICON_WARNING = os.path.join(ICON_ROOT, 'AlertCautionIcon.icns')
  82. ICON_WEB = os.path.join(ICON_ROOT, 'BookmarkIcon.icns')
  83. ####################################################################
  84. # non-ASCII to ASCII diacritic folding.
  85. # Used by `fold_to_ascii` method
  86. ####################################################################
  87. ASCII_REPLACEMENTS = {
  88. 'À': 'A',
  89. 'Á': 'A',
  90. 'Â': 'A',
  91. 'Ã': 'A',
  92. 'Ä': 'A',
  93. 'Å': 'A',
  94. 'Æ': 'AE',
  95. 'Ç': 'C',
  96. 'È': 'E',
  97. 'É': 'E',
  98. 'Ê': 'E',
  99. 'Ë': 'E',
  100. 'Ì': 'I',
  101. 'Í': 'I',
  102. 'Î': 'I',
  103. 'Ï': 'I',
  104. 'Ð': 'D',
  105. 'Ñ': 'N',
  106. 'Ò': 'O',
  107. 'Ó': 'O',
  108. 'Ô': 'O',
  109. 'Õ': 'O',
  110. 'Ö': 'O',
  111. 'Ø': 'O',
  112. 'Ù': 'U',
  113. 'Ú': 'U',
  114. 'Û': 'U',
  115. 'Ü': 'U',
  116. 'Ý': 'Y',
  117. 'Þ': 'Th',
  118. 'ß': 'ss',
  119. 'à': 'a',
  120. 'á': 'a',
  121. 'â': 'a',
  122. 'ã': 'a',
  123. 'ä': 'a',
  124. 'å': 'a',
  125. 'æ': 'ae',
  126. 'ç': 'c',
  127. 'è': 'e',
  128. 'é': 'e',
  129. 'ê': 'e',
  130. 'ë': 'e',
  131. 'ì': 'i',
  132. 'í': 'i',
  133. 'î': 'i',
  134. 'ï': 'i',
  135. 'ð': 'd',
  136. 'ñ': 'n',
  137. 'ò': 'o',
  138. 'ó': 'o',
  139. 'ô': 'o',
  140. 'õ': 'o',
  141. 'ö': 'o',
  142. 'ø': 'o',
  143. 'ù': 'u',
  144. 'ú': 'u',
  145. 'û': 'u',
  146. 'ü': 'u',
  147. 'ý': 'y',
  148. 'þ': 'th',
  149. 'ÿ': 'y',
  150. 'Ł': 'L',
  151. 'ł': 'l',
  152. 'Ń': 'N',
  153. 'ń': 'n',
  154. 'Ņ': 'N',
  155. 'ņ': 'n',
  156. 'Ň': 'N',
  157. 'ň': 'n',
  158. 'Ŋ': 'ng',
  159. 'ŋ': 'NG',
  160. 'Ō': 'O',
  161. 'ō': 'o',
  162. 'Ŏ': 'O',
  163. 'ŏ': 'o',
  164. 'Ő': 'O',
  165. 'ő': 'o',
  166. 'Œ': 'OE',
  167. 'œ': 'oe',
  168. 'Ŕ': 'R',
  169. 'ŕ': 'r',
  170. 'Ŗ': 'R',
  171. 'ŗ': 'r',
  172. 'Ř': 'R',
  173. 'ř': 'r',
  174. 'Ś': 'S',
  175. 'ś': 's',
  176. 'Ŝ': 'S',
  177. 'ŝ': 's',
  178. 'Ş': 'S',
  179. 'ş': 's',
  180. 'Š': 'S',
  181. 'š': 's',
  182. 'Ţ': 'T',
  183. 'ţ': 't',
  184. 'Ť': 'T',
  185. 'ť': 't',
  186. 'Ŧ': 'T',
  187. 'ŧ': 't',
  188. 'Ũ': 'U',
  189. 'ũ': 'u',
  190. 'Ū': 'U',
  191. 'ū': 'u',
  192. 'Ŭ': 'U',
  193. 'ŭ': 'u',
  194. 'Ů': 'U',
  195. 'ů': 'u',
  196. 'Ű': 'U',
  197. 'ű': 'u',
  198. 'Ŵ': 'W',
  199. 'ŵ': 'w',
  200. 'Ŷ': 'Y',
  201. 'ŷ': 'y',
  202. 'Ÿ': 'Y',
  203. 'Ź': 'Z',
  204. 'ź': 'z',
  205. 'Ż': 'Z',
  206. 'ż': 'z',
  207. 'Ž': 'Z',
  208. 'ž': 'z',
  209. 'ſ': 's',
  210. 'Α': 'A',
  211. 'Β': 'B',
  212. 'Γ': 'G',
  213. 'Δ': 'D',
  214. 'Ε': 'E',
  215. 'Ζ': 'Z',
  216. 'Η': 'E',
  217. 'Θ': 'Th',
  218. 'Ι': 'I',
  219. 'Κ': 'K',
  220. 'Λ': 'L',
  221. 'Μ': 'M',
  222. 'Ν': 'N',
  223. 'Ξ': 'Ks',
  224. 'Ο': 'O',
  225. 'Π': 'P',
  226. 'Ρ': 'R',
  227. 'Σ': 'S',
  228. 'Τ': 'T',
  229. 'Υ': 'U',
  230. 'Φ': 'Ph',
  231. 'Χ': 'Kh',
  232. 'Ψ': 'Ps',
  233. 'Ω': 'O',
  234. 'α': 'a',
  235. 'β': 'b',
  236. 'γ': 'g',
  237. 'δ': 'd',
  238. 'ε': 'e',
  239. 'ζ': 'z',
  240. 'η': 'e',
  241. 'θ': 'th',
  242. 'ι': 'i',
  243. 'κ': 'k',
  244. 'λ': 'l',
  245. 'μ': 'm',
  246. 'ν': 'n',
  247. 'ξ': 'x',
  248. 'ο': 'o',
  249. 'π': 'p',
  250. 'ρ': 'r',
  251. 'ς': 's',
  252. 'σ': 's',
  253. 'τ': 't',
  254. 'υ': 'u',
  255. 'φ': 'ph',
  256. 'χ': 'kh',
  257. 'ψ': 'ps',
  258. 'ω': 'o',
  259. 'А': 'A',
  260. 'Б': 'B',
  261. 'В': 'V',
  262. 'Г': 'G',
  263. 'Д': 'D',
  264. 'Е': 'E',
  265. 'Ж': 'Zh',
  266. 'З': 'Z',
  267. 'И': 'I',
  268. 'Й': 'I',
  269. 'К': 'K',
  270. 'Л': 'L',
  271. 'М': 'M',
  272. 'Н': 'N',
  273. 'О': 'O',
  274. 'П': 'P',
  275. 'Р': 'R',
  276. 'С': 'S',
  277. 'Т': 'T',
  278. 'У': 'U',
  279. 'Ф': 'F',
  280. 'Х': 'Kh',
  281. 'Ц': 'Ts',
  282. 'Ч': 'Ch',
  283. 'Ш': 'Sh',
  284. 'Щ': 'Shch',
  285. 'Ъ': "'",
  286. 'Ы': 'Y',
  287. 'Ь': "'",
  288. 'Э': 'E',
  289. 'Ю': 'Iu',
  290. 'Я': 'Ia',
  291. 'а': 'a',
  292. 'б': 'b',
  293. 'в': 'v',
  294. 'г': 'g',
  295. 'д': 'd',
  296. 'е': 'e',
  297. 'ж': 'zh',
  298. 'з': 'z',
  299. 'и': 'i',
  300. 'й': 'i',
  301. 'к': 'k',
  302. 'л': 'l',
  303. 'м': 'm',
  304. 'н': 'n',
  305. 'о': 'o',
  306. 'п': 'p',
  307. 'р': 'r',
  308. 'с': 's',
  309. 'т': 't',
  310. 'у': 'u',
  311. 'ф': 'f',
  312. 'х': 'kh',
  313. 'ц': 'ts',
  314. 'ч': 'ch',
  315. 'ш': 'sh',
  316. 'щ': 'shch',
  317. 'ъ': "'",
  318. 'ы': 'y',
  319. 'ь': "'",
  320. 'э': 'e',
  321. 'ю': 'iu',
  322. 'я': 'ia',
  323. # 'ᴀ': '',
  324. # 'ᴁ': '',
  325. # 'ᴂ': '',
  326. # 'ᴃ': '',
  327. # 'ᴄ': '',
  328. # 'ᴅ': '',
  329. # 'ᴆ': '',
  330. # 'ᴇ': '',
  331. # 'ᴈ': '',
  332. # 'ᴉ': '',
  333. # 'ᴊ': '',
  334. # 'ᴋ': '',
  335. # 'ᴌ': '',
  336. # 'ᴍ': '',
  337. # 'ᴎ': '',
  338. # 'ᴏ': '',
  339. # 'ᴐ': '',
  340. # 'ᴑ': '',
  341. # 'ᴒ': '',
  342. # 'ᴓ': '',
  343. # 'ᴔ': '',
  344. # 'ᴕ': '',
  345. # 'ᴖ': '',
  346. # 'ᴗ': '',
  347. # 'ᴘ': '',
  348. # 'ᴙ': '',
  349. # 'ᴚ': '',
  350. # 'ᴛ': '',
  351. # 'ᴜ': '',
  352. # 'ᴝ': '',
  353. # 'ᴞ': '',
  354. # 'ᴟ': '',
  355. # 'ᴠ': '',
  356. # 'ᴡ': '',
  357. # 'ᴢ': '',
  358. # 'ᴣ': '',
  359. # 'ᴤ': '',
  360. # 'ᴥ': '',
  361. 'ᴦ': 'G',
  362. 'ᴧ': 'L',
  363. 'ᴨ': 'P',
  364. 'ᴩ': 'R',
  365. 'ᴪ': 'PS',
  366. 'ẞ': 'Ss',
  367. 'Ỳ': 'Y',
  368. 'ỳ': 'y',
  369. 'Ỵ': 'Y',
  370. 'ỵ': 'y',
  371. 'Ỹ': 'Y',
  372. 'ỹ': 'y',
  373. }
  374. ####################################################################
  375. # Smart-to-dumb punctuation mapping
  376. ####################################################################
  377. DUMB_PUNCTUATION = {
  378. '‘': "'",
  379. '’': "'",
  380. '‚': "'",
  381. '“': '"',
  382. '”': '"',
  383. '„': '"',
  384. '–': '-',
  385. '—': '-'
  386. }
  387. ####################################################################
  388. # Used by `Workflow.filter`
  389. ####################################################################
  390. # Anchor characters in a name
  391. #: Characters that indicate the beginning of a "word" in CamelCase
  392. INITIALS = string.ascii_uppercase + string.digits
  393. #: Split on non-letters, numbers
  394. split_on_delimiters = re.compile('[^a-zA-Z0-9]').split
  395. # Match filter flags
  396. #: Match items that start with ``query``
  397. MATCH_STARTSWITH = 1
  398. #: Match items whose capital letters start with ``query``
  399. MATCH_CAPITALS = 2
  400. #: Match items with a component "word" that matches ``query``
  401. MATCH_ATOM = 4
  402. #: Match items whose initials (based on atoms) start with ``query``
  403. MATCH_INITIALS_STARTSWITH = 8
  404. #: Match items whose initials (based on atoms) contain ``query``
  405. MATCH_INITIALS_CONTAIN = 16
  406. #: Combination of :const:`MATCH_INITIALS_STARTSWITH` and
  407. #: :const:`MATCH_INITIALS_CONTAIN`
  408. MATCH_INITIALS = 24
  409. #: Match items if ``query`` is a substring
  410. MATCH_SUBSTRING = 32
  411. #: Match items if all characters in ``query`` appear in the item in order
  412. MATCH_ALLCHARS = 64
  413. #: Combination of all other ``MATCH_*`` constants
  414. MATCH_ALL = 127
  415. ####################################################################
  416. # Used by `Workflow.check_update`
  417. ####################################################################
  418. # Number of days to wait between checking for updates to the workflow
  419. DEFAULT_UPDATE_FREQUENCY = 1
  420. ####################################################################
  421. # Keychain access errors
  422. ####################################################################
  423. class KeychainError(Exception):
  424. """Raised for unknown Keychain errors.
  425. Raised by methods :meth:`Workflow.save_password`,
  426. :meth:`Workflow.get_password` and :meth:`Workflow.delete_password`
  427. when ``security`` CLI app returns an unknown error code.
  428. """
  429. class PasswordNotFound(KeychainError):
  430. """Password not in Keychain.
  431. Raised by method :meth:`Workflow.get_password` when ``account``
  432. is unknown to the Keychain.
  433. """
  434. class PasswordExists(KeychainError):
  435. """Raised when trying to overwrite an existing account password.
  436. You should never receive this error: it is used internally
  437. by the :meth:`Workflow.save_password` method to know if it needs
  438. to delete the old password first (a Keychain implementation detail).
  439. """
  440. ####################################################################
  441. # Helper functions
  442. ####################################################################
  443. def isascii(text):
  444. """Test if ``text`` contains only ASCII characters.
  445. :param text: text to test for ASCII-ness
  446. :type text: ``unicode``
  447. :returns: ``True`` if ``text`` contains only ASCII characters
  448. :rtype: ``Boolean``
  449. """
  450. try:
  451. text.encode('ascii')
  452. except UnicodeEncodeError:
  453. return False
  454. return True
  455. ####################################################################
  456. # Implementation classes
  457. ####################################################################
  458. class SerializerManager(object):
  459. """Contains registered serializers.
  460. .. versionadded:: 1.8
  461. A configured instance of this class is available at
  462. :attr:`workflow.manager`.
  463. Use :meth:`register()` to register new (or replace
  464. existing) serializers, which you can specify by name when calling
  465. :class:`~workflow.Workflow` data storage methods.
  466. See :ref:`guide-serialization` and :ref:`guide-persistent-data`
  467. for further information.
  468. """
  469. def __init__(self):
  470. """Create new SerializerManager object."""
  471. self._serializers = {}
  472. def register(self, name, serializer):
  473. """Register ``serializer`` object under ``name``.
  474. Raises :class:`AttributeError` if ``serializer`` in invalid.
  475. .. note::
  476. ``name`` will be used as the file extension of the saved files.
  477. :param name: Name to register ``serializer`` under
  478. :type name: ``unicode`` or ``str``
  479. :param serializer: object with ``load()`` and ``dump()``
  480. methods
  481. """
  482. # Basic validation
  483. getattr(serializer, 'load')
  484. getattr(serializer, 'dump')
  485. self._serializers[name] = serializer
  486. def serializer(self, name):
  487. """Return serializer object for ``name``.
  488. :param name: Name of serializer to return
  489. :type name: ``unicode`` or ``str``
  490. :returns: serializer object or ``None`` if no such serializer
  491. is registered.
  492. """
  493. return self._serializers.get(name)
  494. def unregister(self, name):
  495. """Remove registered serializer with ``name``.
  496. Raises a :class:`ValueError` if there is no such registered
  497. serializer.
  498. :param name: Name of serializer to remove
  499. :type name: ``unicode`` or ``str``
  500. :returns: serializer object
  501. """
  502. if name not in self._serializers:
  503. raise ValueError('No such serializer registered : {0}'.format(
  504. name))
  505. serializer = self._serializers[name]
  506. del self._serializers[name]
  507. return serializer
  508. @property
  509. def serializers(self):
  510. """Return names of registered serializers."""
  511. return sorted(self._serializers.keys())
  512. class JSONSerializer(object):
  513. """Wrapper around :mod:`json`. Sets ``indent`` and ``encoding``.
  514. .. versionadded:: 1.8
  515. Use this serializer if you need readable data files. JSON doesn't
  516. support Python objects as well as ``cPickle``/``pickle``, so be
  517. careful which data you try to serialize as JSON.
  518. """
  519. @classmethod
  520. def load(cls, file_obj):
  521. """Load serialized object from open JSON file.
  522. .. versionadded:: 1.8
  523. :param file_obj: file handle
  524. :type file_obj: ``file`` object
  525. :returns: object loaded from JSON file
  526. :rtype: object
  527. """
  528. return json.load(file_obj)
  529. @classmethod
  530. def dump(cls, obj, file_obj):
  531. """Serialize object ``obj`` to open JSON file.
  532. .. versionadded:: 1.8
  533. :param obj: Python object to serialize
  534. :type obj: JSON-serializable data structure
  535. :param file_obj: file handle
  536. :type file_obj: ``file`` object
  537. """
  538. return json.dump(obj, file_obj, indent=2, encoding='utf-8')
  539. class CPickleSerializer(object):
  540. """Wrapper around :mod:`cPickle`. Sets ``protocol``.
  541. .. versionadded:: 1.8
  542. This is the default serializer and the best combination of speed and
  543. flexibility.
  544. """
  545. @classmethod
  546. def load(cls, file_obj):
  547. """Load serialized object from open pickle file.
  548. .. versionadded:: 1.8
  549. :param file_obj: file handle
  550. :type file_obj: ``file`` object
  551. :returns: object loaded from pickle file
  552. :rtype: object
  553. """
  554. return cPickle.load(file_obj)
  555. @classmethod
  556. def dump(cls, obj, file_obj):
  557. """Serialize object ``obj`` to open pickle file.
  558. .. versionadded:: 1.8
  559. :param obj: Python object to serialize
  560. :type obj: Python object
  561. :param file_obj: file handle
  562. :type file_obj: ``file`` object
  563. """
  564. return cPickle.dump(obj, file_obj, protocol=-1)
  565. class PickleSerializer(object):
  566. """Wrapper around :mod:`pickle`. Sets ``protocol``.
  567. .. versionadded:: 1.8
  568. Use this serializer if you need to add custom pickling.
  569. """
  570. @classmethod
  571. def load(cls, file_obj):
  572. """Load serialized object from open pickle file.
  573. .. versionadded:: 1.8
  574. :param file_obj: file handle
  575. :type file_obj: ``file`` object
  576. :returns: object loaded from pickle file
  577. :rtype: object
  578. """
  579. return pickle.load(file_obj)
  580. @classmethod
  581. def dump(cls, obj, file_obj):
  582. """Serialize object ``obj`` to open pickle file.
  583. .. versionadded:: 1.8
  584. :param obj: Python object to serialize
  585. :type obj: Python object
  586. :param file_obj: file handle
  587. :type file_obj: ``file`` object
  588. """
  589. return pickle.dump(obj, file_obj, protocol=-1)
  590. # Set up default manager and register built-in serializers
  591. manager = SerializerManager()
  592. manager.register('cpickle', CPickleSerializer)
  593. manager.register('pickle', PickleSerializer)
  594. manager.register('json', JSONSerializer)
  595. class Item(object):
  596. """Represents a feedback item for Alfred.
  597. Generates Alfred-compliant XML for a single item.
  598. You probably shouldn't use this class directly, but via
  599. :meth:`Workflow.add_item`. See :meth:`~Workflow.add_item`
  600. for details of arguments.
  601. """
  602. def __init__(self, title, subtitle='', modifier_subtitles=None,
  603. arg=None, autocomplete=None, valid=False, uid=None,
  604. icon=None, icontype=None, type=None, largetext=None,
  605. copytext=None, quicklookurl=None):
  606. """Same arguments as :meth:`Workflow.add_item`."""
  607. self.title = title
  608. self.subtitle = subtitle
  609. self.modifier_subtitles = modifier_subtitles or {}
  610. self.arg = arg
  611. self.autocomplete = autocomplete
  612. self.valid = valid
  613. self.uid = uid
  614. self.icon = icon
  615. self.icontype = icontype
  616. self.type = type
  617. self.largetext = largetext
  618. self.copytext = copytext
  619. self.quicklookurl = quicklookurl
  620. @property
  621. def elem(self):
  622. """Create and return feedback item for Alfred.
  623. :returns: :class:`ElementTree.Element <xml.etree.ElementTree.Element>`
  624. instance for this :class:`Item` instance.
  625. """
  626. # Attributes on <item> element
  627. attr = {}
  628. if self.valid:
  629. attr['valid'] = 'yes'
  630. else:
  631. attr['valid'] = 'no'
  632. # Allow empty string for autocomplete. This is a useful value,
  633. # as TABing the result will revert the query back to just the
  634. # keyword
  635. if self.autocomplete is not None:
  636. attr['autocomplete'] = self.autocomplete
  637. # Optional attributes
  638. for name in ('uid', 'type'):
  639. value = getattr(self, name, None)
  640. if value:
  641. attr[name] = value
  642. root = ET.Element('item', attr)
  643. ET.SubElement(root, 'title').text = self.title
  644. ET.SubElement(root, 'subtitle').text = self.subtitle
  645. # Add modifier subtitles
  646. for mod in ('cmd', 'ctrl', 'alt', 'shift', 'fn'):
  647. if mod in self.modifier_subtitles:
  648. ET.SubElement(root, 'subtitle',
  649. {'mod': mod}).text = self.modifier_subtitles[mod]
  650. # Add arg as element instead of attribute on <item>, as it's more
  651. # flexible (newlines aren't allowed in attributes)
  652. if self.arg:
  653. ET.SubElement(root, 'arg').text = self.arg
  654. # Add icon if there is one
  655. if self.icon:
  656. if self.icontype:
  657. attr = dict(type=self.icontype)
  658. else:
  659. attr = {}
  660. ET.SubElement(root, 'icon', attr).text = self.icon
  661. if self.largetext:
  662. ET.SubElement(root, 'text',
  663. {'type': 'largetype'}).text = self.largetext
  664. if self.copytext:
  665. ET.SubElement(root, 'text',
  666. {'type': 'copy'}).text = self.copytext
  667. if self.quicklookurl:
  668. ET.SubElement(root, 'quicklookurl').text = self.quicklookurl
  669. return root
  670. class Settings(dict):
  671. """A dictionary that saves itself when changed.
  672. Dictionary keys & values will be saved as a JSON file
  673. at ``filepath``. If the file does not exist, the dictionary
  674. (and settings file) will be initialised with ``defaults``.
  675. :param filepath: where to save the settings
  676. :type filepath: :class:`unicode`
  677. :param defaults: dict of default settings
  678. :type defaults: :class:`dict`
  679. An appropriate instance is provided by :class:`Workflow` instances at
  680. :attr:`Workflow.settings`.
  681. """
  682. def __init__(self, filepath, defaults=None):
  683. """Create new :class:`Settings` object."""
  684. super(Settings, self).__init__()
  685. self._filepath = filepath
  686. self._nosave = False
  687. self._original = {}
  688. if os.path.exists(self._filepath):
  689. self._load()
  690. elif defaults:
  691. for key, val in defaults.items():
  692. self[key] = val
  693. self.save() # save default settings
  694. def _load(self):
  695. """Load cached settings from JSON file `self._filepath`."""
  696. data = {}
  697. with LockFile(self._filepath, 0.5):
  698. with open(self._filepath, 'rb') as fp:
  699. data.update(json.load(fp))
  700. self._original = deepcopy(data)
  701. self._nosave = True
  702. self.update(data)
  703. self._nosave = False
  704. @uninterruptible
  705. def save(self):
  706. """Save settings to JSON file specified in ``self._filepath``.
  707. If you're using this class via :attr:`Workflow.settings`, which
  708. you probably are, ``self._filepath`` will be ``settings.json``
  709. in your workflow's data directory (see :attr:`~Workflow.datadir`).
  710. """
  711. if self._nosave:
  712. return
  713. data = {}
  714. data.update(self)
  715. with LockFile(self._filepath, 0.5):
  716. with atomic_writer(self._filepath, 'wb') as fp:
  717. json.dump(data, fp, sort_keys=True, indent=2,
  718. encoding='utf-8')
  719. # dict methods
  720. def __setitem__(self, key, value):
  721. """Implement :class:`dict` interface."""
  722. if self._original.get(key) != value:
  723. super(Settings, self).__setitem__(key, value)
  724. self.save()
  725. def __delitem__(self, key):
  726. """Implement :class:`dict` interface."""
  727. super(Settings, self).__delitem__(key)
  728. self.save()
  729. def update(self, *args, **kwargs):
  730. """Override :class:`dict` method to save on update."""
  731. super(Settings, self).update(*args, **kwargs)
  732. self.save()
  733. def setdefault(self, key, value=None):
  734. """Override :class:`dict` method to save on update."""
  735. ret = super(Settings, self).setdefault(key, value)
  736. self.save()
  737. return ret
  738. class Workflow(object):
  739. """The ``Workflow`` object is the main interface to Alfred-Workflow.
  740. It provides APIs for accessing the Alfred/workflow environment,
  741. storing & caching data, using Keychain, and generating Script
  742. Filter feedback.
  743. ``Workflow`` is compatible with Alfred 2+. Subclass
  744. :class:`~workflow.Workflow3` provides additional features,
  745. only available in Alfred 3+, such as workflow variables.
  746. :param default_settings: default workflow settings. If no settings file
  747. exists, :class:`Workflow.settings` will be pre-populated with
  748. ``default_settings``.
  749. :type default_settings: :class:`dict`
  750. :param update_settings: settings for updating your workflow from
  751. GitHub releases. The only required key is ``github_slug``,
  752. whose value must take the form of ``username/repo``.
  753. If specified, ``Workflow`` will check the repo's releases
  754. for updates. Your workflow must also have a semantic version
  755. number. Please see the :ref:`User Manual <user-manual>` and
  756. `update API docs <api-updates>` for more information.
  757. :type update_settings: :class:`dict`
  758. :param input_encoding: encoding of command line arguments. You
  759. should probably leave this as the default (``utf-8``), which
  760. is the encoding Alfred uses.
  761. :type input_encoding: :class:`unicode`
  762. :param normalization: normalisation to apply to CLI args.
  763. See :meth:`Workflow.decode` for more details.
  764. :type normalization: :class:`unicode`
  765. :param capture_args: Capture and act on ``workflow:*`` arguments. See
  766. :ref:`Magic arguments <magic-arguments>` for details.
  767. :type capture_args: :class:`Boolean`
  768. :param libraries: sequence of paths to directories containing
  769. libraries. These paths will be prepended to ``sys.path``.
  770. :type libraries: :class:`tuple` or :class:`list`
  771. :param help_url: URL to webpage where a user can ask for help with
  772. the workflow, report bugs, etc. This could be the GitHub repo
  773. or a page on AlfredForum.com. If your workflow throws an error,
  774. this URL will be displayed in the log and Alfred's debugger. It can
  775. also be opened directly in a web browser with the ``workflow:help``
  776. :ref:`magic argument <magic-arguments>`.
  777. :type help_url: :class:`unicode` or :class:`str`
  778. """
  779. # Which class to use to generate feedback items. You probably
  780. # won't want to change this
  781. item_class = Item
  782. def __init__(self, default_settings=None, update_settings=None,
  783. input_encoding='utf-8', normalization='NFC',
  784. capture_args=True, libraries=None,
  785. help_url=None):
  786. """Create new :class:`Workflow` object."""
  787. self._default_settings = default_settings or {}
  788. self._update_settings = update_settings or {}
  789. self._input_encoding = input_encoding
  790. self._normalizsation = normalization
  791. self._capture_args = capture_args
  792. self.help_url = help_url
  793. self._workflowdir = None
  794. self._settings_path = None
  795. self._settings = None
  796. self._bundleid = None
  797. self._debugging = None
  798. self._name = None
  799. self._cache_serializer = 'cpickle'
  800. self._data_serializer = 'cpickle'
  801. self._info = None
  802. self._info_loaded = False
  803. self._logger = None
  804. self._items = []
  805. self._alfred_env = None
  806. # Version number of the workflow
  807. self._version = UNSET
  808. # Version from last workflow run
  809. self._last_version_run = UNSET
  810. # Cache for regex patterns created for filter keys
  811. self._search_pattern_cache = {}
  812. #: Prefix for all magic arguments.
  813. #: The default value is ``workflow:`` so keyword
  814. #: ``config`` would match user query ``workflow:config``.
  815. self.magic_prefix = 'workflow:'
  816. #: Mapping of available magic arguments. The built-in magic
  817. #: arguments are registered by default. To add your own magic arguments
  818. #: (or override built-ins), add a key:value pair where the key is
  819. #: what the user should enter (prefixed with :attr:`magic_prefix`)
  820. #: and the value is a callable that will be called when the argument
  821. #: is entered. If you would like to display a message in Alfred, the
  822. #: function should return a ``unicode`` string.
  823. #:
  824. #: By default, the magic arguments documented
  825. #: :ref:`here <magic-arguments>` are registered.
  826. self.magic_arguments = {}
  827. self._register_default_magic()
  828. if libraries:
  829. sys.path = libraries + sys.path
  830. ####################################################################
  831. # API methods
  832. ####################################################################
  833. # info.plist contents and alfred_* environment variables ----------
  834. @property
  835. def alfred_version(self):
  836. """Alfred version as :class:`~workflow.update.Version` object."""
  837. from update import Version
  838. return Version(self.alfred_env.get('version'))
  839. @property
  840. def alfred_env(self):
  841. """Dict of Alfred's environmental variables minus ``alfred_`` prefix.
  842. .. versionadded:: 1.7
  843. The variables Alfred 2.4+ exports are:
  844. ============================ =========================================
  845. Variable Description
  846. ============================ =========================================
  847. debug Set to ``1`` if Alfred's debugger is
  848. open, otherwise unset.
  849. preferences Path to Alfred.alfredpreferences
  850. (where your workflows and settings are
  851. stored).
  852. preferences_localhash Machine-specific preferences are stored
  853. in ``Alfred.alfredpreferences/preferences/local/<hash>``
  854. (see ``preferences`` above for
  855. the path to ``Alfred.alfredpreferences``)
  856. theme ID of selected theme
  857. theme_background Background colour of selected theme in
  858. format ``rgba(r,g,b,a)``
  859. theme_subtext Show result subtext.
  860. ``0`` = Always,
  861. ``1`` = Alternative actions only,
  862. ``2`` = Selected result only,
  863. ``3`` = Never
  864. version Alfred version number, e.g. ``'2.4'``
  865. version_build Alfred build number, e.g. ``277``
  866. workflow_bundleid Bundle ID, e.g.
  867. ``net.deanishe.alfred-mailto``
  868. workflow_cache Path to workflow's cache directory
  869. workflow_data Path to workflow's data directory
  870. workflow_name Name of current workflow
  871. workflow_uid UID of workflow
  872. workflow_version The version number specified in the
  873. workflow configuration sheet/info.plist
  874. ============================ =========================================
  875. **Note:** all values are Unicode strings except ``version_build`` and
  876. ``theme_subtext``, which are integers.
  877. :returns: ``dict`` of Alfred's environmental variables without the
  878. ``alfred_`` prefix, e.g. ``preferences``, ``workflow_data``.
  879. """
  880. if self._alfred_env is not None:
  881. return self._alfred_env
  882. data = {}
  883. for key in (
  884. 'debug',
  885. 'preferences',
  886. 'preferences_localhash',
  887. 'theme',
  888. 'theme_background',
  889. 'theme_subtext',
  890. 'version',
  891. 'version_build',
  892. 'workflow_bundleid',
  893. 'workflow_cache',
  894. 'workflow_data',
  895. 'workflow_name',
  896. 'workflow_uid',
  897. 'workflow_version'):
  898. value = os.getenv('alfred_' + key, '')
  899. if value:
  900. if key in ('debug', 'version_build', 'theme_subtext'):
  901. value = int(value)
  902. else:
  903. value = self.decode(value)
  904. data[key] = value
  905. self._alfred_env = data
  906. return self._alfred_env
  907. @property
  908. def info(self):
  909. """:class:`dict` of ``info.plist`` contents."""
  910. if not self._info_loaded:
  911. self._load_info_plist()
  912. return self._info
  913. @property
  914. def bundleid(self):
  915. """Workflow bundle ID from environmental vars or ``info.plist``.
  916. :returns: bundle ID
  917. :rtype: ``unicode``
  918. """
  919. if not self._bundleid:
  920. if self.alfred_env.get('workflow_bundleid'):
  921. self._bundleid = self.alfred_env.get('workflow_bundleid')
  922. else:
  923. self._bundleid = unicode(self.info['bundleid'], 'utf-8')
  924. return self._bundleid
  925. @property
  926. def debugging(self):
  927. """Whether Alfred's debugger is open.
  928. :returns: ``True`` if Alfred's debugger is open.
  929. :rtype: ``bool``
  930. """
  931. return self.alfred_env.get('debug') == 1
  932. @property
  933. def name(self):
  934. """Workflow name from Alfred's environmental vars or ``info.plist``.
  935. :returns: workflow name
  936. :rtype: ``unicode``
  937. """
  938. if not self._name:
  939. if self.alfred_env.get('workflow_name'):
  940. self._name = self.decode(self.alfred_env.get('workflow_name'))
  941. else:
  942. self._name = self.decode(self.info['name'])
  943. return self._name
  944. @property
  945. def version(self):
  946. """Return the version of the workflow.
  947. .. versionadded:: 1.9.10
  948. Get the workflow version from environment variable,
  949. the ``update_settings`` dict passed on
  950. instantiation, the ``version`` file located in the workflow's
  951. root directory or ``info.plist``. Return ``None`` if none
  952. exists or :class:`ValueError` if the version number is invalid
  953. (i.e. not semantic).
  954. :returns: Version of the workflow (not Alfred-Workflow)
  955. :rtype: :class:`~workflow.update.Version` object
  956. """
  957. if self._version is UNSET:
  958. version = None
  959. # environment variable has priority
  960. if self.alfred_env.get('workflow_version'):
  961. version = self.alfred_env['workflow_version']
  962. # Try `update_settings`
  963. elif self._update_settings:
  964. version = self._update_settings.get('version')
  965. # `version` file
  966. if not version:
  967. filepath = self.workflowfile('version')
  968. if os.path.exists(filepath):
  969. with open(filepath, 'rb') as fileobj:
  970. version = fileobj.read()
  971. # info.plist
  972. if not version:
  973. version = self.info.get('version')
  974. if version:
  975. from update import Version
  976. version = Version(version)
  977. self._version = version
  978. return self._version
  979. # Workflow utility methods -----------------------------------------
  980. @property
  981. def args(self):
  982. """Return command line args as normalised unicode.
  983. Args are decoded and normalised via :meth:`~Workflow.decode`.
  984. The encoding and normalisation are the ``input_encoding`` and
  985. ``normalization`` arguments passed to :class:`Workflow` (``UTF-8``
  986. and ``NFC`` are the defaults).
  987. If :class:`Workflow` is called with ``capture_args=True``
  988. (the default), :class:`Workflow` will look for certain
  989. ``workflow:*`` args and, if found, perform the corresponding
  990. actions and exit the workflow.
  991. See :ref:`Magic arguments <magic-arguments>` for details.
  992. """
  993. msg = None
  994. args = [self.decode(arg) for arg in sys.argv[1:]]
  995. # Handle magic args
  996. if len(args) and self._capture_args:
  997. for name in self.magic_arguments:
  998. key = '{0}{1}'.format(self.magic_prefix, name)
  999. if key in args:
  1000. msg = self.magic_arguments[name]()
  1001. if msg:
  1002. self.logger.debug(msg)
  1003. if not sys.stdout.isatty(): # Show message in Alfred
  1004. self.add_item(msg, valid=False, icon=ICON_INFO)
  1005. self.send_feedback()
  1006. sys.exit(0)
  1007. return args
  1008. @property
  1009. def cachedir(self):
  1010. """Path to workflow's cache directory.
  1011. The cache directory is a subdirectory of Alfred's own cache directory
  1012. in ``~/Library/Caches``. The full path is in Alfred 4+ is:
  1013. ``~/Library/Caches/com.runningwithcrayons.Alfred/Workflow Data/<bundle id>``
  1014. For earlier versions:
  1015. ``~/Library/Caches/com.runningwithcrayons.Alfred-X/Workflow Data/<bundle id>``
  1016. where ``Alfred-X`` may be ``Alfred-2`` or ``Alfred-3``.
  1017. Returns:
  1018. unicode: full path to workflow's cache directory
  1019. """
  1020. if self.alfred_env.get('workflow_cache'):
  1021. dirpath = self.alfred_env.get('workflow_cache')
  1022. else:
  1023. dirpath = self._default_cachedir
  1024. return self._create(dirpath)
  1025. @property
  1026. def _default_cachedir(self):
  1027. """Alfred 2's default cache directory."""
  1028. return os.path.join(
  1029. os.path.expanduser(
  1030. '~/Library/Caches/com.runningwithcrayons.Alfred-2/'
  1031. 'Workflow Data/'),
  1032. self.bundleid)
  1033. @property
  1034. def datadir(self):
  1035. """Path to workflow's data directory.
  1036. The data directory is a subdirectory of Alfred's own data directory in
  1037. ``~/Library/Application Support``. The full path for Alfred 4+ is:
  1038. ``~/Library/Application Support/Alfred/Workflow Data/<bundle id>``
  1039. For earlier versions, the path is:
  1040. ``~/Library/Application Support/Alfred X/Workflow Data/<bundle id>``
  1041. where ``Alfred X` is ``Alfred 2`` or ``Alfred 3``.
  1042. Returns:
  1043. unicode: full path to workflow data directory
  1044. """
  1045. if self.alfred_env.get('workflow_data'):
  1046. dirpath = self.alfred_env.get('workflow_data')
  1047. else:
  1048. dirpath = self._default_datadir
  1049. return self._create(dirpath)
  1050. @property
  1051. def _default_datadir(self):
  1052. """Alfred 2's default data directory."""
  1053. return os.path.join(os.path.expanduser(
  1054. '~/Library/Application Support/Alfred 2/Workflow Data/'),
  1055. self.bundleid)
  1056. @property
  1057. def workflowdir(self):
  1058. """Path to workflow's root directory (where ``info.plist`` is).
  1059. Returns:
  1060. unicode: full path to workflow root directory
  1061. """
  1062. if not self._workflowdir:
  1063. # Try the working directory first, then the directory
  1064. # the library is in. CWD will be the workflow root if
  1065. # a workflow is being run in Alfred
  1066. candidates = [
  1067. os.path.abspath(os.getcwdu()),
  1068. os.path.dirname(os.path.abspath(os.path.dirname(__file__)))]
  1069. # climb the directory tree until we find `info.plist`
  1070. for dirpath in candidates:
  1071. # Ensure directory path is Unicode
  1072. dirpath = self.decode(dirpath)
  1073. while True:
  1074. if os.path.exists(os.path.join(dirpath, 'info.plist')):
  1075. self._workflowdir = dirpath
  1076. break
  1077. elif dirpath == '/':
  1078. # no `info.plist` found
  1079. break
  1080. # Check the parent directory
  1081. dirpath = os.path.dirname(dirpath)
  1082. # No need to check other candidates
  1083. if self._workflowdir:
  1084. break
  1085. if not self._workflowdir:
  1086. raise IOError("'info.plist' not found in directory tree")
  1087. return self._workflowdir
  1088. def cachefile(self, filename):
  1089. """Path to ``filename`` in workflow's cache directory.
  1090. Return absolute path to ``filename`` within your workflow's
  1091. :attr:`cache directory <Workflow.cachedir>`.
  1092. :param filename: basename of file
  1093. :type filename: ``unicode``
  1094. :returns: full path to file within cache directory
  1095. :rtype: ``unicode``
  1096. """
  1097. return os.path.join(self.cachedir, filename)
  1098. def datafile(self, filename):
  1099. """Path to ``filename`` in workflow's data directory.
  1100. Return absolute path to ``filename`` within your workflow's
  1101. :attr:`data directory <Workflow.datadir>`.
  1102. :param filename: basename of file
  1103. :type filename: ``unicode``
  1104. :returns: full path to file within data directory
  1105. :rtype: ``unicode``
  1106. """
  1107. return os.path.join(self.datadir, filename)
  1108. def workflowfile(self, filename):
  1109. """Return full path to ``filename`` in workflow's root directory.
  1110. :param filename: basename of file
  1111. :type filename: ``unicode``
  1112. :returns: full path to file within data directory
  1113. :rtype: ``unicode``
  1114. """
  1115. return os.path.join(self.workflowdir, filename)
  1116. @property
  1117. def logfile(self):
  1118. """Path to logfile.
  1119. :returns: path to logfile within workflow's cache directory
  1120. :rtype: ``unicode``
  1121. """
  1122. return self.cachefile('%s.log' % self.bundleid)
  1123. @property
  1124. def logger(self):
  1125. """Logger that logs to both console and a log file.
  1126. If Alfred's debugger is open, log level will be ``DEBUG``,
  1127. else it will be ``INFO``.
  1128. Use :meth:`open_log` to open the log file in Console.
  1129. :returns: an initialised :class:`~logging.Logger`
  1130. """
  1131. if self._logger:
  1132. return self._logger
  1133. # Initialise new logger and optionally handlers
  1134. logger = logging.getLogger('')
  1135. # Only add one set of handlers
  1136. # Exclude from coverage, as pytest will have configured the
  1137. # root logger already
  1138. if not len(logger.handlers): # pragma: no cover
  1139. fmt = logging.Formatter(
  1140. '%(asctime)s %(filename)s:%(lineno)s'
  1141. ' %(levelname)-8s %(message)s',
  1142. datefmt='%H:%M:%S')
  1143. logfile = logging.handlers.RotatingFileHandler(
  1144. self.logfile,
  1145. maxBytes=1024 * 1024,
  1146. backupCount=1)
  1147. logfile.setFormatter(fmt)
  1148. logger.addHandler(logfile)
  1149. console = logging.StreamHandler()
  1150. console.setFormatter(fmt)
  1151. logger.addHandler(console)
  1152. if self.debugging:
  1153. logger.setLevel(logging.DEBUG)
  1154. else:
  1155. logger.setLevel(logging.INFO)
  1156. self._logger = logger
  1157. return self._logger
  1158. @logger.setter
  1159. def logger(self, logger):
  1160. """Set a custom logger.
  1161. :param logger: The logger to use
  1162. :type logger: `~logging.Logger` instance
  1163. """
  1164. self._logger = logger
  1165. @property
  1166. def settings_path(self):
  1167. """Path to settings file within workflow's data directory.
  1168. :returns: path to ``settings.json`` file
  1169. :rtype: ``unicode``
  1170. """
  1171. if not self._settings_path:
  1172. self._settings_path = self.datafile('settings.json')
  1173. return self._settings_path
  1174. @property
  1175. def settings(self):
  1176. """Return a dictionary subclass that saves itself when changed.
  1177. See :ref:`guide-settings` in the :ref:`user-manual` for more
  1178. information on how to use :attr:`settings` and **important
  1179. limitations** on what it can do.
  1180. :returns: :class:`~workflow.workflow.Settings` instance
  1181. initialised from the data in JSON file at
  1182. :attr:`settings_path` or if that doesn't exist, with the
  1183. ``default_settings`` :class:`dict` passed to
  1184. :class:`Workflow` on instantiation.
  1185. :rtype: :class:`~workflow.workflow.Settings` instance
  1186. """
  1187. if not self._settings:
  1188. self.logger.debug('reading settings from %s', self.settings_path)
  1189. self._settings = Settings(self.settings_path,
  1190. self._default_settings)
  1191. return self._settings
  1192. @property
  1193. def cache_serializer(self):
  1194. """Name of default cache serializer.
  1195. .. versionadded:: 1.8
  1196. This serializer is used by :meth:`cache_data()` and
  1197. :meth:`cached_data()`
  1198. See :class:`SerializerManager` for details.
  1199. :returns: serializer name
  1200. :rtype: ``unicode``
  1201. """
  1202. return self._cache_serializer
  1203. @cache_serializer.setter
  1204. def cache_serializer(self, serializer_name):
  1205. """Set the default cache serialization format.
  1206. .. versionadded:: 1.8
  1207. This serializer is used by :meth:`cache_data()` and
  1208. :meth:`cached_data()`
  1209. The specified serializer must already by registered with the
  1210. :class:`SerializerManager` at `~workflow.workflow.manager`,
  1211. otherwise a :class:`ValueError` will be raised.
  1212. :param serializer_name: Name of default serializer to use.
  1213. :type serializer_name:
  1214. """
  1215. if manager.serializer(serializer_name) is None:
  1216. raise ValueError(
  1217. 'Unknown serializer : `{0}`. Register your serializer '
  1218. 'with `manager` first.'.format(serializer_name))
  1219. self.logger.debug('default cache serializer: %s', serializer_name)
  1220. self._cache_serializer = serializer_name
  1221. @property
  1222. def data_serializer(self):
  1223. """Name of default data serializer.
  1224. .. versionadded:: 1.8
  1225. This serializer is used by :meth:`store_data()` and
  1226. :meth:`stored_data()`
  1227. See :class:`SerializerManager` for details.
  1228. :returns: serializer name
  1229. :rtype: ``unicode``
  1230. """
  1231. return self._data_serializer
  1232. @data_serializer.setter
  1233. def data_serializer(self, serializer_name):
  1234. """Set the default cache serialization format.
  1235. .. versionadded:: 1.8
  1236. This serializer is used by :meth:`store_data()` and
  1237. :meth:`stored_data()`
  1238. The specified serializer must already by registered with the
  1239. :class:`SerializerManager` at `~workflow.workflow.manager`,
  1240. otherwise a :class:`ValueError` will be raised.
  1241. :param serializer_name: Name of serializer to use by default.
  1242. """
  1243. if manager.serializer(serializer_name) is None:
  1244. raise ValueError(
  1245. 'Unknown serializer : `{0}`. Register your serializer '
  1246. 'with `manager` first.'.format(serializer_name))
  1247. self.logger.debug('default data serializer: %s', serializer_name)
  1248. self._data_serializer = serializer_name
  1249. def stored_data(self, name):
  1250. """Retrieve data from data directory.
  1251. Returns ``None`` if there are no data stored under ``name``.
  1252. .. versionadded:: 1.8
  1253. :param name: name of datastore
  1254. """
  1255. metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
  1256. if not os.path.exists(metadata_path):
  1257. self.logger.debug('no data stored for `%s`', name)
  1258. return None
  1259. with open(metadata_path, 'rb') as file_obj:
  1260. serializer_name = file_obj.read().strip()
  1261. serializer = manager.serializer(serializer_name)
  1262. if serializer is None:
  1263. raise ValueError(
  1264. 'Unknown serializer `{0}`. Register a corresponding '
  1265. 'serializer with `manager.register()` '
  1266. 'to load this data.'.format(serializer_name))
  1267. self.logger.debug('data `%s` stored as `%s`', name, serializer_name)
  1268. filename = '{0}.{1}'.format(name, serializer_name)
  1269. data_path = self.datafile(filename)
  1270. if not os.path.exists(data_path):
  1271. self.logger.debug('no data stored: %s', name)
  1272. if os.path.exists(metadata_path):
  1273. os.unlink(metadata_path)
  1274. return None
  1275. with open(data_path, 'rb') as file_obj:
  1276. data = serializer.load(file_obj)
  1277. self.logger.debug('stored data loaded: %s', data_path)
  1278. return data
  1279. def store_data(self, name, data, serializer=None):
  1280. """Save data to data directory.
  1281. .. versionadded:: 1.8
  1282. If ``data`` is ``None``, the datastore will be deleted.
  1283. Note that the datastore does NOT support mutliple threads.
  1284. :param name: name of datastore
  1285. :param data: object(s) to store. **Note:** some serializers
  1286. can only handled certain types of data.
  1287. :param serializer: name of serializer to use. If no serializer
  1288. is specified, the default will be used. See
  1289. :class:`SerializerManager` for more information.
  1290. :returns: data in datastore or ``None``
  1291. """
  1292. # Ensure deletion is not interrupted by SIGTERM
  1293. @uninterruptible
  1294. def delete_paths(paths):
  1295. """Clear one or more data stores"""
  1296. for path in paths:
  1297. if os.path.exists(path):
  1298. os.unlink(path)
  1299. self.logger.debug('deleted data file: %s', path)
  1300. serializer_name = serializer or self.data_serializer
  1301. # In order for `stored_data()` to be able to load data stored with
  1302. # an arbitrary serializer, yet still have meaningful file extensions,
  1303. # the format (i.e. extension) is saved to an accompanying file
  1304. metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
  1305. filename = '{0}.{1}'.format(name, serializer_name)
  1306. data_path = self.datafile(filename)
  1307. if data_path == self.settings_path:
  1308. raise ValueError(
  1309. 'Cannot save data to' +
  1310. '`{0}` with format `{1}`. '.format(name, serializer_name) +
  1311. "This would overwrite Alfred-Workflow's settings file.")
  1312. serializer = manager.serializer(serializer_name)
  1313. if serializer is None:
  1314. raise ValueError(
  1315. 'Invalid serializer `{0}`. Register your serializer with '
  1316. '`manager.register()` first.'.format(serializer_name))
  1317. if data is None: # Delete cached data
  1318. delete_paths((metadata_path, data_path))
  1319. return
  1320. # Ensure write is not interrupted by SIGTERM
  1321. @uninterruptible
  1322. def _store():
  1323. # Save file extension
  1324. with atomic_writer(metadata_path, 'wb') as file_obj:
  1325. file_obj.write(serializer_name)
  1326. with atomic_writer(data_path, 'wb') as file_obj:
  1327. serializer.dump(data, file_obj)
  1328. _store()
  1329. self.logger.debug('saved data: %s', data_path)
  1330. def cached_data(self, name, data_func=None, max_age=60):
  1331. """Return cached data if younger than ``max_age`` seconds.
  1332. Retrieve data from cache or re-generate and re-cache data if
  1333. stale/non-existant. If ``max_age`` is 0, return cached data no
  1334. matter how old.
  1335. :param name: name of datastore
  1336. :param data_func: function to (re-)generate data.
  1337. :type data_func: ``callable``
  1338. :param max_age: maximum age of cached data in seconds
  1339. :type max_age: ``int``
  1340. :returns: cached data, return value of ``data_func`` or ``None``
  1341. if ``data_func`` is not set
  1342. """
  1343. serializer = manager.serializer(self.cache_serializer)
  1344. cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
  1345. age = self.cached_data_age(name)
  1346. if (age < max_age or max_age == 0) and os.path.exists(cache_path):
  1347. with open(cache_path, 'rb') as file_obj:
  1348. self.logger.debug('loading cached data: %s', cache_path)
  1349. return serializer.load(file_obj)
  1350. if not data_func:
  1351. return None
  1352. data = data_func()
  1353. self.cache_data(name, data)
  1354. return data
  1355. def cache_data(self, name, data):
  1356. """Save ``data`` to cache under ``name``.
  1357. If ``data`` is ``None``, the corresponding cache file will be
  1358. deleted.
  1359. :param name: name of datastore
  1360. :param data: data to store. This may be any object supported by
  1361. the cache serializer
  1362. """
  1363. serializer = manager.serializer(self.cache_serializer)
  1364. cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
  1365. if data is None:
  1366. if os.path.exists(cache_path):
  1367. os.unlink(cache_path)
  1368. self.logger.debug('deleted cache file: %s', cache_path)
  1369. return
  1370. with atomic_writer(cache_path, 'wb') as file_obj:
  1371. serializer.dump(data, file_obj)
  1372. self.logger.debug('cached data: %s', cache_path)
  1373. def cached_data_fresh(self, name, max_age):
  1374. """Whether cache `name` is less than `max_age` seconds old.
  1375. :param name: name of datastore
  1376. :param max_age: maximum age of data in seconds
  1377. :type max_age: ``int``
  1378. :returns: ``True`` if data is less than ``max_age`` old, else
  1379. ``False``
  1380. """
  1381. age = self.cached_data_age(name)
  1382. if not age:
  1383. return False
  1384. return age < max_age
  1385. def cached_data_age(self, name):
  1386. """Return age in seconds of cache `name` or 0 if cache doesn't exist.
  1387. :param name: name of datastore
  1388. :type name: ``unicode``
  1389. :returns: age of datastore in seconds
  1390. :rtype: ``int``
  1391. """
  1392. cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
  1393. if not os.path.exists(cache_path):
  1394. return 0
  1395. return time.time() - os.stat(cache_path).st_mtime
  1396. def filter(self, query, items, key=lambda x: x, ascending=False,
  1397. include_score=False, min_score=0, max_results=0,
  1398. match_on=MATCH_ALL, fold_diacritics=True):
  1399. """Fuzzy search filter. Returns list of ``items`` that match ``query``.
  1400. ``query`` is case-insensitive. Any item that does not contain the
  1401. entirety of ``query`` is rejected.
  1402. If ``query`` is an empty string or contains only whitespace,
  1403. all items will match.
  1404. :param query: query to test items against
  1405. :type query: ``unicode``
  1406. :param items: iterable of items to test
  1407. :type items: ``list`` or ``tuple``
  1408. :param key: function to get comparison key from ``items``.
  1409. Must return a ``unicode`` string. The default simply returns
  1410. the item.
  1411. :type key: ``callable``
  1412. :param ascending: set to ``True`` to get worst matches first
  1413. :type ascending: ``Boolean``
  1414. :param include_score: Useful for debugging the scoring algorithm.
  1415. If ``True``, results will be a list of tuples
  1416. ``(item, score, rule)``.
  1417. :type include_score: ``Boolean``
  1418. :param min_score: If non-zero, ignore results with a score lower
  1419. than this.
  1420. :type min_score: ``int``
  1421. :param max_results: If non-zero, prune results list to this length.
  1422. :type max_results: ``int``
  1423. :param match_on: Filter option flags. Bitwise-combined list of
  1424. ``MATCH_*`` constants (see below).
  1425. :type match_on: ``int``
  1426. :param fold_diacritics: Convert search keys to ASCII-only
  1427. characters if ``query`` only contains ASCII characters.
  1428. :type fold_diacritics: ``Boolean``
  1429. :returns: list of ``items`` matching ``query`` or list of
  1430. ``(item, score, rule)`` `tuples` if ``include_score`` is ``True``.
  1431. ``rule`` is the ``MATCH_*`` rule that matched the item.
  1432. :rtype: ``list``
  1433. **Matching rules**
  1434. By default, :meth:`filter` uses all of the following flags (i.e.
  1435. :const:`MATCH_ALL`). The tests are always run in the given order:
  1436. 1. :const:`MATCH_STARTSWITH`
  1437. Item search key starts with ``query`` (case-insensitive).
  1438. 2. :const:`MATCH_CAPITALS`
  1439. The list of capital letters in item search key starts with
  1440. ``query`` (``query`` may be lower-case). E.g., ``of``
  1441. would match ``OmniFocus``, ``gc`` would match ``Google Chrome``.
  1442. 3. :const:`MATCH_ATOM`
  1443. Search key is split into "atoms" on non-word characters
  1444. (.,-,' etc.). Matches if ``query`` is one of these atoms
  1445. (case-insensitive).
  1446. 4. :const:`MATCH_INITIALS_STARTSWITH`
  1447. Initials are the first characters of the above-described
  1448. "atoms" (case-insensitive).
  1449. 5. :const:`MATCH_INITIALS_CONTAIN`
  1450. ``query`` is a substring of the above-described initials.
  1451. 6. :const:`MATCH_INITIALS`
  1452. Combination of (4) and (5).
  1453. 7. :const:`MATCH_SUBSTRING`
  1454. ``query`` is a substring of item search key (case-insensitive).
  1455. 8. :const:`MATCH_ALLCHARS`
  1456. All characters in ``query`` appear in item search key in
  1457. the same order (case-insensitive).
  1458. 9. :const:`MATCH_ALL`
  1459. Combination of all the above.
  1460. :const:`MATCH_ALLCHARS` is considerably slower than the other
  1461. tests and provides much less accurate results.
  1462. **Examples:**
  1463. To ignore :const:`MATCH_ALLCHARS` (tends to provide the worst
  1464. matches and is expensive to run), use
  1465. ``match_on=MATCH_ALL ^ MATCH_ALLCHARS``.
  1466. To match only on capitals, use ``match_on=MATCH_CAPITALS``.
  1467. To match only on startswith and substring, use
  1468. ``match_on=MATCH_STARTSWITH | MATCH_SUBSTRING``.
  1469. **Diacritic folding**
  1470. .. versionadded:: 1.3
  1471. If ``fold_diacritics`` is ``True`` (the default), and ``query``
  1472. contains only ASCII characters, non-ASCII characters in search keys
  1473. will be converted to ASCII equivalents (e.g. **ü** -> **u**,
  1474. **ß** -> **ss**, **é** -> **e**).
  1475. See :const:`ASCII_REPLACEMENTS` for all replacements.
  1476. If ``query`` contains non-ASCII characters, search keys will not be
  1477. altered.
  1478. """
  1479. if not query:
  1480. return items
  1481. # Remove preceding/trailing spaces
  1482. query = query.strip()
  1483. if not query:
  1484. return items
  1485. # Use user override if there is one
  1486. fold_diacritics = self.settings.get('__workflow_diacritic_folding',
  1487. fold_diacritics)
  1488. results = []
  1489. for item in items:
  1490. skip = False
  1491. score = 0
  1492. words = [s.strip() for s in query.split(' ')]
  1493. value = key(item).strip()
  1494. if value == '':
  1495. continue
  1496. for word in words:
  1497. if word == '':
  1498. continue
  1499. s, rule = self._filter_item(value, word, match_on,
  1500. fold_diacritics)
  1501. if not s: # Skip items that don't match part of the query
  1502. skip = True
  1503. score += s
  1504. if skip:
  1505. continue
  1506. if score:
  1507. # use "reversed" `score` (i.e. highest becomes lowest) and
  1508. # `value` as sort key. This means items with the same score
  1509. # will be sorted in alphabetical not reverse alphabetical order
  1510. results.append(((100.0 / score, value.lower(), score),
  1511. (item, score, rule)))
  1512. # sort on keys, then discard the keys
  1513. results.sort(reverse=ascending)
  1514. results = [t[1] for t in results]
  1515. if min_score:
  1516. results = [r for r in results if r[1] > min_score]
  1517. if max_results and len(results) > max_results:
  1518. results = results[:max_results]
  1519. # return list of ``(item, score, rule)``
  1520. if include_score:
  1521. return results
  1522. # just return list of items
  1523. return [t[0] for t in results]
  1524. def _filter_item(self, value, query, match_on, fold_diacritics):
  1525. """Filter ``value`` against ``query`` using rules ``match_on``.
  1526. :returns: ``(score, rule)``
  1527. """
  1528. query = query.lower()
  1529. if not isascii(query):
  1530. fold_diacritics = False
  1531. if fold_diacritics:
  1532. value = self.fold_to_ascii(value)
  1533. # pre-filter any items that do not contain all characters
  1534. # of ``query`` to save on running several more expensive tests
  1535. if not set(query) <= set(value.lower()):
  1536. return (0, None)
  1537. # item starts with query
  1538. if match_on & MATCH_STARTSWITH and value.lower().startswith(query):
  1539. score = 100.0 - (len(value) / len(query))
  1540. return (score, MATCH_STARTSWITH)
  1541. # query matches capitalised letters in item,
  1542. # e.g. of = OmniFocus
  1543. if match_on & MATCH_CAPITALS:
  1544. initials = ''.join([c for c in value if c in INITIALS])
  1545. if initials.lower().startswith(query):
  1546. score = 100.0 - (len(initials) / len(query))
  1547. return (score, MATCH_CAPITALS)
  1548. # split the item into "atoms", i.e. words separated by
  1549. # spaces or other non-word characters
  1550. if (match_on & MATCH_ATOM or
  1551. match_on & MATCH_INITIALS_CONTAIN or
  1552. match_on & MATCH_INITIALS_STARTSWITH):
  1553. atoms = [s.lower() for s in split_on_delimiters(value)]
  1554. # print('atoms : %s --> %s' % (value, atoms))
  1555. # initials of the atoms
  1556. initials = ''.join([s[0] for s in atoms if s])
  1557. if match_on & MATCH_ATOM:
  1558. # is `query` one of the atoms in item?
  1559. # similar to substring, but scores more highly, as it's
  1560. # a word within the item
  1561. if query in atoms:
  1562. score = 100.0 - (len(value) / len(query))
  1563. return (score, MATCH_ATOM)
  1564. # `query` matches start (or all) of the initials of the
  1565. # atoms, e.g. ``himym`` matches "How I Met Your Mother"
  1566. # *and* "how i met your mother" (the ``capitals`` rule only
  1567. # matches the former)
  1568. if (match_on & MATCH_INITIALS_STARTSWITH and
  1569. initials.startswith(query)):
  1570. score = 100.0 - (len(initials) / len(query))
  1571. return (score, MATCH_INITIALS_STARTSWITH)
  1572. # `query` is a substring of initials, e.g. ``doh`` matches
  1573. # "The Dukes of Hazzard"
  1574. elif (match_on & MATCH_INITIALS_CONTAIN and
  1575. query in initials):
  1576. score = 95.0 - (len(initials) / len(query))
  1577. return (score, MATCH_INITIALS_CONTAIN)
  1578. # `query` is a substring of item
  1579. if match_on & MATCH_SUBSTRING and query in value.lower():
  1580. score = 90.0 - (len(value) / len(query))
  1581. return (score, MATCH_SUBSTRING)
  1582. # finally, assign a score based on how close together the
  1583. # characters in `query` are in item.
  1584. if match_on & MATCH_ALLCHARS:
  1585. search = self._search_for_query(query)
  1586. match = search(value)
  1587. if match:
  1588. score = 100.0 / ((1 + match.start()) *
  1589. (match.end() - match.start() + 1))
  1590. return (score, MATCH_ALLCHARS)
  1591. # Nothing matched
  1592. return (0, None)
  1593. def _search_for_query(self, query):
  1594. if query in self._search_pattern_cache:
  1595. return self._search_pattern_cache[query]
  1596. # Build pattern: include all characters
  1597. pattern = []
  1598. for c in query:
  1599. # pattern.append('[^{0}]*{0}'.format(re.escape(c)))
  1600. pattern.append('.*?{0}'.format(re.escape(c)))
  1601. pattern = ''.join(pattern)
  1602. search = re.compile(pattern, re.IGNORECASE).search
  1603. self._search_pattern_cache[query] = search
  1604. return search
  1605. def run(self, func, text_errors=False):
  1606. """Call ``func`` to run your workflow.
  1607. :param func: Callable to call with ``self`` (i.e. the :class:`Workflow`
  1608. instance) as first argument.
  1609. :param text_errors: Emit error messages in plain text, not in
  1610. Alfred's XML/JSON feedback format. Use this when you're not
  1611. running Alfred-Workflow in a Script Filter and would like
  1612. to pass the error message to, say, a notification.
  1613. :type text_errors: ``Boolean``
  1614. ``func`` will be called with :class:`Workflow` instance as first
  1615. argument.
  1616. ``func`` should be the main entry point to your workflow.
  1617. Any exceptions raised will be logged and an error message will be
  1618. output to Alfred.
  1619. """
  1620. start = time.time()
  1621. # Write to debugger to ensure "real" output starts on a new line
  1622. print('.', file=sys.stderr)
  1623. # Call workflow's entry function/method within a try-except block
  1624. # to catch any errors and display an error message in Alfred
  1625. try:
  1626. if self.version:
  1627. self.logger.debug('---------- %s (%s) ----------',
  1628. self.name, self.version)
  1629. else:
  1630. self.logger.debug('---------- %s ----------', self.name)
  1631. # Run update check if configured for self-updates.
  1632. # This call has to go in the `run` try-except block, as it will
  1633. # initialise `self.settings`, which will raise an exception
  1634. # if `settings.json` isn't valid.
  1635. if self._update_settings:
  1636. self.check_update()
  1637. # Run workflow's entry function/method
  1638. func(self)
  1639. # Set last version run to current version after a successful
  1640. # run
  1641. self.set_last_version()
  1642. except Exception as err:
  1643. self.logger.exception(err)
  1644. if self.help_url:
  1645. self.logger.info('for assistance, see: %s', self.help_url)
  1646. if not sys.stdout.isatty(): # Show error in Alfred
  1647. if text_errors:
  1648. print(unicode(err).encode('utf-8'), end='')
  1649. else:
  1650. self._items = []
  1651. if self._name:
  1652. name = self._name
  1653. elif self._bundleid: # pragma: no cover
  1654. name = self._bundleid
  1655. else: # pragma: no cover
  1656. name = os.path.dirname(__file__)
  1657. self.add_item("Error in workflow '%s'" % name,
  1658. unicode(err),
  1659. icon=ICON_ERROR)
  1660. self.send_feedback()
  1661. return 1
  1662. finally:
  1663. self.logger.debug('---------- finished in %0.3fs ----------',
  1664. time.time() - start)
  1665. return 0
  1666. # Alfred feedback methods ------------------------------------------
  1667. def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None,
  1668. autocomplete=None, valid=False, uid=None, icon=None,
  1669. icontype=None, type=None, largetext=None, copytext=None,
  1670. quicklookurl=None):
  1671. """Add an item to be output to Alfred.
  1672. :param title: Title shown in Alfred
  1673. :type title: ``unicode``
  1674. :param subtitle: Subtitle shown in Alfred
  1675. :type subtitle: ``unicode``
  1676. :param modifier_subtitles: Subtitles shown when modifier
  1677. (CMD, OPT etc.) is pressed. Use a ``dict`` with the lowercase
  1678. keys ``cmd``, ``ctrl``, ``shift``, ``alt`` and ``fn``
  1679. :type modifier_subtitles: ``dict``
  1680. :param arg: Argument passed by Alfred as ``{query}`` when item is
  1681. actioned
  1682. :type arg: ``unicode``
  1683. :param autocomplete: Text expanded in Alfred when item is TABbed
  1684. :type autocomplete: ``unicode``
  1685. :param valid: Whether or not item can be actioned
  1686. :type valid: ``Boolean``
  1687. :param uid: Used by Alfred to remember/sort items
  1688. :type uid: ``unicode``
  1689. :param icon: Filename of icon to use
  1690. :type icon: ``unicode``
  1691. :param icontype: Type of icon. Must be one of ``None`` , ``'filetype'``
  1692. or ``'fileicon'``. Use ``'filetype'`` when ``icon`` is a filetype
  1693. such as ``'public.folder'``. Use ``'fileicon'`` when you wish to
  1694. use the icon of the file specified as ``icon``, e.g.
  1695. ``icon='/Applications/Safari.app', icontype='fileicon'``.
  1696. Leave as `None` if ``icon`` points to an actual
  1697. icon file.
  1698. :type icontype: ``unicode``
  1699. :param type: Result type. Currently only ``'file'`` is supported
  1700. (by Alfred). This will tell Alfred to enable file actions for
  1701. this item.
  1702. :type type: ``unicode``
  1703. :param largetext: Text to be displayed in Alfred's large text box
  1704. if user presses CMD+L on item.
  1705. :type largetext: ``unicode``
  1706. :param copytext: Text to be copied to pasteboard if user presses
  1707. CMD+C on item.
  1708. :type copytext: ``unicode``
  1709. :param quicklookurl: URL to be displayed using Alfred's Quick Look
  1710. feature (tapping ``SHIFT`` or ``⌘+Y`` on a result).
  1711. :type quicklookurl: ``unicode``
  1712. :returns: :class:`Item` instance
  1713. See :ref:`icons` for a list of the supported system icons.
  1714. .. note::
  1715. Although this method returns an :class:`Item` instance, you don't
  1716. need to hold onto it or worry about it. All generated :class:`Item`
  1717. instances are also collected internally and sent to Alfred when
  1718. :meth:`send_feedback` is called.
  1719. The generated :class:`Item` is only returned in case you want to
  1720. edit it or do something with it other than send it to Alfred.
  1721. """
  1722. item = self.item_class(title, subtitle, modifier_subtitles, arg,
  1723. autocomplete, valid, uid, icon, icontype, type,
  1724. largetext, copytext, quicklookurl)
  1725. self._items.append(item)
  1726. return item
  1727. def send_feedback(self):
  1728. """Print stored items to console/Alfred as XML."""
  1729. root = ET.Element('items')
  1730. for item in self._items:
  1731. root.append(item.elem)
  1732. sys.stdout.write('<?xml version="1.0" encoding="utf-8"?>\n')
  1733. sys.stdout.write(ET.tostring(root).encode('utf-8'))
  1734. sys.stdout.flush()
  1735. ####################################################################
  1736. # Updating methods
  1737. ####################################################################
  1738. @property
  1739. def first_run(self):
  1740. """Return ``True`` if it's the first time this version has run.
  1741. .. versionadded:: 1.9.10
  1742. Raises a :class:`ValueError` if :attr:`version` isn't set.
  1743. """
  1744. if not self.version:
  1745. raise ValueError('No workflow version set')
  1746. if not self.last_version_run:
  1747. return True
  1748. return self.version != self.last_version_run
  1749. @property
  1750. def last_version_run(self):
  1751. """Return version of last version to run (or ``None``).
  1752. .. versionadded:: 1.9.10
  1753. :returns: :class:`~workflow.update.Version` instance
  1754. or ``None``
  1755. """
  1756. if self._last_version_run is UNSET:
  1757. version = self.settings.get('__workflow_last_version')
  1758. if version:
  1759. from update import Version
  1760. version = Version(version)
  1761. self._last_version_run = version
  1762. self.logger.debug('last run version: %s', self._last_version_run)
  1763. return self._last_version_run
  1764. def set_last_version(self, version=None):
  1765. """Set :attr:`last_version_run` to current version.
  1766. .. versionadded:: 1.9.10
  1767. :param version: version to store (default is current version)
  1768. :type version: :class:`~workflow.update.Version` instance
  1769. or ``unicode``
  1770. :returns: ``True`` if version is saved, else ``False``
  1771. """
  1772. if not version:
  1773. if not self.version:
  1774. self.logger.warning(
  1775. "Can't save last version: workflow has no version")
  1776. return False
  1777. version = self.version
  1778. if isinstance(version, basestring):
  1779. from update import Version
  1780. version = Version(version)
  1781. self.settings['__workflow_last_version'] = str(version)
  1782. self.logger.debug('set last run version: %s', version)
  1783. return True
  1784. @property
  1785. def update_available(self):
  1786. """Whether an update is available.
  1787. .. versionadded:: 1.9
  1788. See :ref:`guide-updates` in the :ref:`user-manual` for detailed
  1789. information on how to enable your workflow to update itself.
  1790. :returns: ``True`` if an update is available, else ``False``
  1791. """
  1792. key = '__workflow_latest_version'
  1793. # Create a new workflow object to ensure standard serialiser
  1794. # is used (update.py is called without the user's settings)
  1795. status = Workflow().cached_data(key, max_age=0)
  1796. # self.logger.debug('update status: %r', status)
  1797. if not status or not status.get('available'):
  1798. return False
  1799. return status['available']
  1800. @property
  1801. def prereleases(self):
  1802. """Whether workflow should update to pre-release versions.
  1803. .. versionadded:: 1.16
  1804. :returns: ``True`` if pre-releases are enabled with the :ref:`magic
  1805. argument <magic-arguments>` or the ``update_settings`` dict, else
  1806. ``False``.
  1807. """
  1808. if self._update_settings.get('prereleases'):
  1809. return True
  1810. return self.settings.get('__workflow_prereleases') or False
  1811. def check_update(self, force=False):
  1812. """Call update script if it's time to check for a new release.
  1813. .. versionadded:: 1.9
  1814. The update script will be run in the background, so it won't
  1815. interfere in the execution of your workflow.
  1816. See :ref:`guide-updates` in the :ref:`user-manual` for detailed
  1817. information on how to enable your workflow to update itself.
  1818. :param force: Force update check
  1819. :type force: ``Boolean``
  1820. """
  1821. key = '__workflow_latest_version'
  1822. frequency = self._update_settings.get('frequency',
  1823. DEFAULT_UPDATE_FREQUENCY)
  1824. if not force and not self.settings.get('__workflow_autoupdate', True):
  1825. self.logger.debug('Auto update turned off by user')
  1826. return
  1827. # Check for new version if it's time
  1828. if (force or not self.cached_data_fresh(key, frequency * 86400)):
  1829. repo = self._update_settings['github_slug']
  1830. # version = self._update_settings['version']
  1831. version = str(self.version)
  1832. from background import run_in_background
  1833. # update.py is adjacent to this file
  1834. update_script = os.path.join(os.path.dirname(__file__),
  1835. b'update.py')
  1836. cmd = ['/usr/bin/python', update_script, 'check', repo, version]
  1837. if self.prereleases:
  1838. cmd.append('--prereleases')
  1839. self.logger.info('checking for update ...')
  1840. run_in_background('__workflow_update_check', cmd)
  1841. else:
  1842. self.logger.debug('update check not due')
  1843. def start_update(self):
  1844. """Check for update and download and install new workflow file.
  1845. .. versionadded:: 1.9
  1846. See :ref:`guide-updates` in the :ref:`user-manual` for detailed
  1847. information on how to enable your workflow to update itself.
  1848. :returns: ``True`` if an update is available and will be
  1849. installed, else ``False``
  1850. """
  1851. import update
  1852. repo = self._update_settings['github_slug']
  1853. # version = self._update_settings['version']
  1854. version = str(self.version)
  1855. if not update.check_update(repo, version, self.prereleases):
  1856. return False
  1857. from background import run_in_background
  1858. # update.py is adjacent to this file
  1859. update_script = os.path.join(os.path.dirname(__file__),
  1860. b'update.py')
  1861. cmd = ['/usr/bin/python', update_script, 'install', repo, version]
  1862. if self.prereleases:
  1863. cmd.append('--prereleases')
  1864. self.logger.debug('downloading update ...')
  1865. run_in_background('__workflow_update_install', cmd)
  1866. return True
  1867. ####################################################################
  1868. # Keychain password storage methods
  1869. ####################################################################
  1870. def save_password(self, account, password, service=None):
  1871. """Save account credentials.
  1872. If the account exists, the old password will first be deleted
  1873. (Keychain throws an error otherwise).
  1874. If something goes wrong, a :class:`KeychainError` exception will
  1875. be raised.
  1876. :param account: name of the account the password is for, e.g.
  1877. "Pinboard"
  1878. :type account: ``unicode``
  1879. :param password: the password to secure
  1880. :type password: ``unicode``
  1881. :param service: Name of the service. By default, this is the
  1882. workflow's bundle ID
  1883. :type service: ``unicode``
  1884. """
  1885. if not service:
  1886. service = self.bundleid
  1887. try:
  1888. self._call_security('add-generic-password', service, account,
  1889. '-w', password)
  1890. self.logger.debug('saved password : %s:%s', service, account)
  1891. except PasswordExists:
  1892. self.logger.debug('password exists : %s:%s', service, account)
  1893. current_password = self.get_password(account, service)
  1894. if current_password == password:
  1895. self.logger.debug('password unchanged')
  1896. else:
  1897. self.delete_password(account, service)
  1898. self._call_security('add-generic-password', service,
  1899. account, '-w', password)
  1900. self.logger.debug('save_password : %s:%s', service, account)
  1901. def get_password(self, account, service=None):
  1902. """Retrieve the password saved at ``service/account``.
  1903. Raise :class:`PasswordNotFound` exception if password doesn't exist.
  1904. :param account: name of the account the password is for, e.g.
  1905. "Pinboard"
  1906. :type account: ``unicode``
  1907. :param service: Name of the service. By default, this is the workflow's
  1908. bundle ID
  1909. :type service: ``unicode``
  1910. :returns: account password
  1911. :rtype: ``unicode``
  1912. """
  1913. if not service:
  1914. service = self.bundleid
  1915. output = self._call_security('find-generic-password', service,
  1916. account, '-g')
  1917. # Parsing of `security` output is adapted from python-keyring
  1918. # by Jason R. Coombs
  1919. # https://pypi.python.org/pypi/keyring
  1920. m = re.search(
  1921. r'password:\s*(?:0x(?P<hex>[0-9A-F]+)\s*)?(?:"(?P<pw>.*)")?',
  1922. output)
  1923. if m:
  1924. groups = m.groupdict()
  1925. h = groups.get('hex')
  1926. password = groups.get('pw')
  1927. if h:
  1928. password = unicode(binascii.unhexlify(h), 'utf-8')
  1929. self.logger.debug('got password : %s:%s', service, account)
  1930. return password
  1931. def delete_password(self, account, service=None):
  1932. """Delete the password stored at ``service/account``.
  1933. Raise :class:`PasswordNotFound` if account is unknown.
  1934. :param account: name of the account the password is for, e.g.
  1935. "Pinboard"
  1936. :type account: ``unicode``
  1937. :param service: Name of the service. By default, this is the workflow's
  1938. bundle ID
  1939. :type service: ``unicode``
  1940. """
  1941. if not service:
  1942. service = self.bundleid
  1943. self._call_security('delete-generic-password', service, account)
  1944. self.logger.debug('deleted password : %s:%s', service, account)
  1945. ####################################################################
  1946. # Methods for workflow:* magic args
  1947. ####################################################################
  1948. def _register_default_magic(self):
  1949. """Register the built-in magic arguments."""
  1950. # TODO: refactor & simplify
  1951. # Wrap callback and message with callable
  1952. def callback(func, msg):
  1953. def wrapper():
  1954. func()
  1955. return msg
  1956. return wrapper
  1957. self.magic_arguments['delcache'] = callback(self.clear_cache,
  1958. 'Deleted workflow cache')
  1959. self.magic_arguments['deldata'] = callback(self.clear_data,
  1960. 'Deleted workflow data')
  1961. self.magic_arguments['delsettings'] = callback(
  1962. self.clear_settings, 'Deleted workflow settings')
  1963. self.magic_arguments['reset'] = callback(self.reset,
  1964. 'Reset workflow')
  1965. self.magic_arguments['openlog'] = callback(self.open_log,
  1966. 'Opening workflow log file')
  1967. self.magic_arguments['opencache'] = callback(
  1968. self.open_cachedir, 'Opening workflow cache directory')
  1969. self.magic_arguments['opendata'] = callback(
  1970. self.open_datadir, 'Opening workflow data directory')
  1971. self.magic_arguments['openworkflow'] = callback(
  1972. self.open_workflowdir, 'Opening workflow directory')
  1973. self.magic_arguments['openterm'] = callback(
  1974. self.open_terminal, 'Opening workflow root directory in Terminal')
  1975. # Diacritic folding
  1976. def fold_on():
  1977. self.settings['__workflow_diacritic_folding'] = True
  1978. return 'Diacritics will always be folded'
  1979. def fold_off():
  1980. self.settings['__workflow_diacritic_folding'] = False
  1981. return 'Diacritics will never be folded'
  1982. def fold_default():
  1983. if '__workflow_diacritic_folding' in self.settings:
  1984. del self.settings['__workflow_diacritic_folding']
  1985. return 'Diacritics folding reset'
  1986. self.magic_arguments['foldingon'] = fold_on
  1987. self.magic_arguments['foldingoff'] = fold_off
  1988. self.magic_arguments['foldingdefault'] = fold_default
  1989. # Updates
  1990. def update_on():
  1991. self.settings['__workflow_autoupdate'] = True
  1992. return 'Auto update turned on'
  1993. def update_off():
  1994. self.settings['__workflow_autoupdate'] = False
  1995. return 'Auto update turned off'
  1996. def prereleases_on():
  1997. self.settings['__workflow_prereleases'] = True
  1998. return 'Prerelease updates turned on'
  1999. def prereleases_off():
  2000. self.settings['__workflow_prereleases'] = False
  2001. return 'Prerelease updates turned off'
  2002. def do_update():
  2003. if self.start_update():
  2004. return 'Downloading and installing update ...'
  2005. else:
  2006. return 'No update available'
  2007. self.magic_arguments['autoupdate'] = update_on
  2008. self.magic_arguments['noautoupdate'] = update_off
  2009. self.magic_arguments['prereleases'] = prereleases_on
  2010. self.magic_arguments['noprereleases'] = prereleases_off
  2011. self.magic_arguments['update'] = do_update
  2012. # Help
  2013. def do_help():
  2014. if self.help_url:
  2015. self.open_help()
  2016. return 'Opening workflow help URL in browser'
  2017. else:
  2018. return 'Workflow has no help URL'
  2019. def show_version():
  2020. if self.version:
  2021. return 'Version: {0}'.format(self.version)
  2022. else:
  2023. return 'This workflow has no version number'
  2024. def list_magic():
  2025. """Display all available magic args in Alfred."""
  2026. isatty = sys.stderr.isatty()
  2027. for name in sorted(self.magic_arguments.keys()):
  2028. if name == 'magic':
  2029. continue
  2030. arg = self.magic_prefix + name
  2031. self.logger.debug(arg)
  2032. if not isatty:
  2033. self.add_item(arg, icon=ICON_INFO)
  2034. if not isatty:
  2035. self.send_feedback()
  2036. self.magic_arguments['help'] = do_help
  2037. self.magic_arguments['magic'] = list_magic
  2038. self.magic_arguments['version'] = show_version
  2039. def clear_cache(self, filter_func=lambda f: True):
  2040. """Delete all files in workflow's :attr:`cachedir`.
  2041. :param filter_func: Callable to determine whether a file should be
  2042. deleted or not. ``filter_func`` is called with the filename
  2043. of each file in the data directory. If it returns ``True``,
  2044. the file will be deleted.
  2045. By default, *all* files will be deleted.
  2046. :type filter_func: ``callable``
  2047. """
  2048. self._delete_directory_contents(self.cachedir, filter_func)
  2049. def clear_data(self, filter_func=lambda f: True):
  2050. """Delete all files in workflow's :attr:`datadir`.
  2051. :param filter_func: Callable to determine whether a file should be
  2052. deleted or not. ``filter_func`` is called with the filename
  2053. of each file in the data directory. If it returns ``True``,
  2054. the file will be deleted.
  2055. By default, *all* files will be deleted.
  2056. :type filter_func: ``callable``
  2057. """
  2058. self._delete_directory_contents(self.datadir, filter_func)
  2059. def clear_settings(self):
  2060. """Delete workflow's :attr:`settings_path`."""
  2061. if os.path.exists(self.settings_path):
  2062. os.unlink(self.settings_path)
  2063. self.logger.debug('deleted : %r', self.settings_path)
  2064. def reset(self):
  2065. """Delete workflow settings, cache and data.
  2066. File :attr:`settings <settings_path>` and directories
  2067. :attr:`cache <cachedir>` and :attr:`data <datadir>` are deleted.
  2068. """
  2069. self.clear_cache()
  2070. self.clear_data()
  2071. self.clear_settings()
  2072. def open_log(self):
  2073. """Open :attr:`logfile` in default app (usually Console.app)."""
  2074. subprocess.call(['open', self.logfile]) # nosec
  2075. def open_cachedir(self):
  2076. """Open the workflow's :attr:`cachedir` in Finder."""
  2077. subprocess.call(['open', self.cachedir]) # nosec
  2078. def open_datadir(self):
  2079. """Open the workflow's :attr:`datadir` in Finder."""
  2080. subprocess.call(['open', self.datadir]) # nosec
  2081. def open_workflowdir(self):
  2082. """Open the workflow's :attr:`workflowdir` in Finder."""
  2083. subprocess.call(['open', self.workflowdir]) # nosec
  2084. def open_terminal(self):
  2085. """Open a Terminal window at workflow's :attr:`workflowdir`."""
  2086. subprocess.call(['open', '-a', 'Terminal', self.workflowdir]) # nosec
  2087. def open_help(self):
  2088. """Open :attr:`help_url` in default browser."""
  2089. subprocess.call(['open', self.help_url]) # nosec
  2090. return 'Opening workflow help URL in browser'
  2091. ####################################################################
  2092. # Helper methods
  2093. ####################################################################
  2094. def decode(self, text, encoding=None, normalization=None):
  2095. """Return ``text`` as normalised unicode.
  2096. If ``encoding`` and/or ``normalization`` is ``None``, the
  2097. ``input_encoding``and ``normalization`` parameters passed to
  2098. :class:`Workflow` are used.
  2099. :param text: string
  2100. :type text: encoded or Unicode string. If ``text`` is already a
  2101. Unicode string, it will only be normalised.
  2102. :param encoding: The text encoding to use to decode ``text`` to
  2103. Unicode.
  2104. :type encoding: ``unicode`` or ``None``
  2105. :param normalization: The nomalisation form to apply to ``text``.
  2106. :type normalization: ``unicode`` or ``None``
  2107. :returns: decoded and normalised ``unicode``
  2108. :class:`Workflow` uses "NFC" normalisation by default. This is the
  2109. standard for Python and will work well with data from the web (via
  2110. :mod:`~workflow.web` or :mod:`json`).
  2111. macOS, on the other hand, uses "NFD" normalisation (nearly), so data
  2112. coming from the system (e.g. via :mod:`subprocess` or
  2113. :func:`os.listdir`/:mod:`os.path`) may not match. You should either
  2114. normalise this data, too, or change the default normalisation used by
  2115. :class:`Workflow`.
  2116. """
  2117. encoding = encoding or self._input_encoding
  2118. normalization = normalization or self._normalizsation
  2119. if not isinstance(text, unicode):
  2120. text = unicode(text, encoding)
  2121. return unicodedata.normalize(normalization, text)
  2122. def fold_to_ascii(self, text):
  2123. """Convert non-ASCII characters to closest ASCII equivalent.
  2124. .. versionadded:: 1.3
  2125. .. note:: This only works for a subset of European languages.
  2126. :param text: text to convert
  2127. :type text: ``unicode``
  2128. :returns: text containing only ASCII characters
  2129. :rtype: ``unicode``
  2130. """
  2131. if isascii(text):
  2132. return text
  2133. text = ''.join([ASCII_REPLACEMENTS.get(c, c) for c in text])
  2134. return unicode(unicodedata.normalize('NFKD',
  2135. text).encode('ascii', 'ignore'))
  2136. def dumbify_punctuation(self, text):
  2137. """Convert non-ASCII punctuation to closest ASCII equivalent.
  2138. This method replaces "smart" quotes and n- or m-dashes with their
  2139. workaday ASCII equivalents. This method is currently not used
  2140. internally, but exists as a helper method for workflow authors.
  2141. .. versionadded: 1.9.7
  2142. :param text: text to convert
  2143. :type text: ``unicode``
  2144. :returns: text with only ASCII punctuation
  2145. :rtype: ``unicode``
  2146. """
  2147. if isascii(text):
  2148. return text
  2149. text = ''.join([DUMB_PUNCTUATION.get(c, c) for c in text])
  2150. return text
  2151. def _delete_directory_contents(self, dirpath, filter_func):
  2152. """Delete all files in a directory.
  2153. :param dirpath: path to directory to clear
  2154. :type dirpath: ``unicode`` or ``str``
  2155. :param filter_func function to determine whether a file shall be
  2156. deleted or not.
  2157. :type filter_func ``callable``
  2158. """
  2159. if os.path.exists(dirpath):
  2160. for filename in os.listdir(dirpath):
  2161. if not filter_func(filename):
  2162. continue
  2163. path = os.path.join(dirpath, filename)
  2164. if os.path.isdir(path):
  2165. shutil.rmtree(path)
  2166. else:
  2167. os.unlink(path)
  2168. self.logger.debug('deleted : %r', path)
  2169. def _load_info_plist(self):
  2170. """Load workflow info from ``info.plist``."""
  2171. # info.plist should be in the directory above this one
  2172. self._info = plistlib.readPlist(self.workflowfile('info.plist'))
  2173. self._info_loaded = True
  2174. def _create(self, dirpath):
  2175. """Create directory `dirpath` if it doesn't exist.
  2176. :param dirpath: path to directory
  2177. :type dirpath: ``unicode``
  2178. :returns: ``dirpath`` argument
  2179. :rtype: ``unicode``
  2180. """
  2181. if not os.path.exists(dirpath):
  2182. os.makedirs(dirpath)
  2183. return dirpath
  2184. def _call_security(self, action, service, account, *args):
  2185. """Call ``security`` CLI program that provides access to keychains.
  2186. May raise `PasswordNotFound`, `PasswordExists` or `KeychainError`
  2187. exceptions (the first two are subclasses of `KeychainError`).
  2188. :param action: The ``security`` action to call, e.g.
  2189. ``add-generic-password``
  2190. :type action: ``unicode``
  2191. :param service: Name of the service.
  2192. :type service: ``unicode``
  2193. :param account: name of the account the password is for, e.g.
  2194. "Pinboard"
  2195. :type account: ``unicode``
  2196. :param password: the password to secure
  2197. :type password: ``unicode``
  2198. :param *args: list of command line arguments to be passed to
  2199. ``security``
  2200. :type *args: `list` or `tuple`
  2201. :returns: ``(retcode, output)``. ``retcode`` is an `int`, ``output`` a
  2202. ``unicode`` string.
  2203. :rtype: `tuple` (`int`, ``unicode``)
  2204. """
  2205. cmd = ['security', action, '-s', service, '-a', account] + list(args)
  2206. p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
  2207. stderr=subprocess.STDOUT)
  2208. stdout, _ = p.communicate()
  2209. if p.returncode == 44: # password does not exist
  2210. raise PasswordNotFound()
  2211. elif p.returncode == 45: # password already exists
  2212. raise PasswordExists()
  2213. elif p.returncode > 0:
  2214. err = KeychainError('Unknown Keychain error : %s' % stdout)
  2215. err.retcode = p.returncode
  2216. raise err
  2217. return stdout.strip().decode('utf-8')