workflow.py 96 KB

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