Jump to content

emmanuel

Member
  • Posts

    12
  • Joined

  • Last visited

Reputation Activity

  1. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Why don't you upload the working script yourself?
     
    FWIW, it would be a good idea to put the full source of the workflow (including icon and info.plist) into the repo (and perhaps add an exported version under Releases).
     
    Currently, there's no way for someone to get a working version of the workflow from GitHub. Nor is there a link to anywhere they could download one.
     
    Personally, I usually keep the full workflow source in a src subdirectory, so I can put other stuff, like an exported version of the workflow and a demo GIF, in the repo, too.
  2. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Very kind of you. Thanks very much.
  3. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Uhhh … I dunno. Click the "buy me a beer" link in my sig? I like beer. 3 cans of Weizenbier for just 1 euro at the supermarket round the corner.
     
    Man, I love Germany.
     
  4. Like
    emmanuel got a reaction from milopus in ESV Online Bible   
    Thank you both for contributing to this workflow!

    I've been meaning to publish this to GitHub but never got around to it. Now that it's evident there's a larger community surrounding this (both here on the forum and others e-mailing me), I'll put it up asap! The ESV API requires an API key that has been approved for use. Feel free to use the key that is present. It's been approved as this workflow's key. I think publishing the API key should be fine. I'll add some comments about proper/ethical use of the key. Let me know if any of you have suggestions on this though!
     
    @deanishe Is there a way to "bundle" these packages into the Alfred workflow so that users won't need to follow extra instructions during installation?
     
    @JGC You're totally right that the input "pr 31:30" behaves incorrectly, oops! Looks like the test verses I used didn't account for the returned output structure of these specially-formatted, multi-line passages from the API. You're more than welcome to try to tackle this and submit a PR once I publish the code !
  5. Thanks
    emmanuel got a reaction from milopus in ESV Online Bible   
    This workflow allows you to quickly look up bible passages and copy them to your clipboard. It submits passage reference queries to the wonderful ESV API (https://api.esv.org/) and parses the response. The API is smart enough to handle many variations and abbreviations!
     
    Download and use it here: ESV Online Bible (on Packal)
     
    GitHub repo: https://github.com/emmanueljl/ESV-Bible-Alfred-workflow
     

  6. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    It's not.
     
     
    I can!
     
    It is indeed a problem with the old version of Python (most likely in the TLS/SNI handling, which has always been crap on Python). Python 2.7.13 works just fine.
     
    There are two realistic options to fix it, imo: use curl or use requests to make the HTTP requests.
     
    Here's the requests version:
    # encoding: utf-8 from __future__ import print_function import json import sys import requests rawInput = sys.argv[1] key = "<redacted>" options = { 'include-passage-references': 'false', 'include-first-verse-numbers': 'false', 'include-verse-numbers': 'false', 'include-footnotes': 'false', 'include-footnote-body': 'false', 'include-short-copyright': 'false', 'include-passage-horizontal-lines': 'false', 'include-heading-horizontal-lines': 'false', 'include-headings': 'false', 'include-selahs': 'false', 'indent-paragraphs': '0', 'indent-poetry': 'false', 'indent-poetry-lines': '0', 'indent-declares': '0', 'indent-psalm-doxology': '0' } headers = { 'Accept': 'application/json', 'Authorization': 'Token ' + key, } baseUrl = 'https://api.esv.org/v3/passage/text/' def exit_with_error(query, errMsg): """Show an error message in Alfred and exit script.""" errorOutput = { "items": [ {"title": query, "subtitle": errMsg} ] } print(json.dumps(errorOutput)) sys.exit(0) # Combine query and options. Requests will take care of URL-encoding params = dict(q=rawInput) params.update(options) # Execute request r = requests.get(baseUrl, params, headers=headers) try: # Raise exception if request failed r.raise_for_status() # Parse response content = r.json() except Exception as err: exit_with_error(rawInput, str(err)) if content['canonical'] == '' or len(content['passages']) > 1: exit_with_error(rawInput, 'No passage found') passageRef = content['canonical'] pageSplit = content['passages'][0].split('\n\n') passage = ' '.join(pageSplit).rstrip() # print passage passageWithRef = passage + ' (' + passageRef + ' ESV)' data = { 'items': [{ 'arg': passageWithRef, 'valid': 'YES', 'autocomplete': passageRef, 'title': passageRef, 'text': {'largetype': passageWithRef}, # Show whole passage on CMD+L 'subtitle': passage }] } print(json.dumps(data))  
  7. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    requests isn't part of Python: you have to install it.

    Assuming you have pip installed, right-click on the workflow in Alfred Preferences and choose Open in Terminal. Then enter pip install --target=. requests to install requests in the workflow.
     
     
    What do you mean "run either version"?
     
    The workflow will always run with the built-in Python unless you edit it to point at the newer version you installed.
  8. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Right. My bad: I was using a newer Python.
     
    In the workflow directory again, install pyOpenSSL with pip install --target=. pyOpenSSL.

    requests should then use that library instead of Python's built-in one, and all should be well.
     
  9. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Sure. Just follow the pip instructions I gave, install the libraries in the workflow. That way, when you export it, the libraries will be included.

    TBH, I think curl would be a better fit, though. Adding all the SSL libraries makes the workflow 13MB…

    Let me see about using curl instead.
  10. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Try this script instead. It uses /usr/bin/curl for HTTP requests. No need for megabytes of additional libraries.
    # encoding: utf-8 from __future__ import print_function import json import sys import subprocess from urllib import urlencode rawInput = sys.argv[1] key = "5974948d3baa3d1cabc4eb00e4099e3d785d43df" options = { 'include-passage-references': 'false', 'include-first-verse-numbers': 'false', 'include-verse-numbers': 'false', 'include-footnotes': 'false', 'include-footnote-body': 'false', 'include-short-copyright': 'false', 'include-passage-horizontal-lines': 'false', 'include-heading-horizontal-lines': 'false', 'include-headings': 'false', 'include-selahs': 'false', 'indent-paragraphs': '0', 'indent-poetry': 'false', 'indent-poetry-lines': '0', 'indent-declares': '0', 'indent-psalm-doxology': '0' } headers = { 'Accept': 'application/json', 'Authorization': 'Token ' + key, } baseUrl = 'https://api.esv.org/v3/passage/text/' def fetch_url(url, params, headers): """Fetch a URL using cURL and parse response as JSON.""" # Encode GET parameters and add to URL qs = urlencode(params) url = url + '?' + qs # Build cURL command cmd = ['/usr/bin/curl', '-sSL', url] for k, v in headers.items(): cmd.extend(['-H', '{}: {}'.format(k, v)]) # Run command and parse response output = subprocess.check_output(cmd) return json.loads(output) def exit_with_error(query, errMsg): """Show an error message in Alfred and exit script.""" errorOutput = { "items": [ {"title": query, "subtitle": errMsg} ] } json.dump(errorOutput, sys.stdout) sys.exit(0) # Combine query and options into GET parameters params = dict(q=rawInput) params.update(options) # Execute request try: content = fetch_url(baseUrl, params, headers) except Exception as err: exit_with_error(rawInput, str(err)) if content['canonical'] == '' or len(content['passages']) > 1: exit_with_error(rawInput, 'No passage found') passageRef = content['canonical'] pageSplit = content['passages'][0].split('\n\n') passage = ' '.join(pageSplit).rstrip() # print passage passageWithRef = passage + ' (' + passageRef + ' ESV)' data = { 'items': [{ 'arg': passageWithRef, 'valid': 'YES', 'autocomplete': passageRef, 'title': passageRef, 'text': {'largetype': passageWithRef}, # Show whole passage on CMD+L 'subtitle': passage }] } json.dump(data, sys.stdout)  
  11. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    One feature I'd consider, and it's suggested in the API docs, is caching the results of queries, at least for a few days.
     
    The verses/passages returned by the API aren't going to change that often, and fetching something from the network is a lot slower than fetching it from disk. Also, it could potentially reduce the usage of the API key quite a lot.
     
    (Though I've no idea how often people are likely to re-enter the same query. I've just been repeatedly mashing in the one from the screenshot because Bible verses aren't really my thing.)
     
  12. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    I noticed an encoding error in my script when I entered "job" as the query (probably something to do with "smart" quotes).
     
    If you don't mind, I "Pythonified" the script and implemented the caching I mentioned. The script below catches and displays API errors (apparently the API returns a detail field if something went wrong), handles non-ASCII text, and caches the results for queries for 40 days (by default), which takes the response time from ~0.8 seconds to ~0.02 seconds for responses that are in the cache.

    Hitting ⌘L on the result will show the passage in Alfred's Large Type window, and ⌘C will copy the same to the clipboard.
     
    So if you'd like to, try this:
    # encoding: utf-8 """Alfred Script Filter to search the ESV Bible.""" from __future__ import print_function from hashlib import md5 import json import os import re import subprocess import sys from time import time from urllib import urlencode API_KEY = '<redacted>' API_URL = 'https://api.esv.org/v3/passage/text/' API_OPTIONS = { 'include-passage-references': 'false', 'include-first-verse-numbers': 'false', 'include-verse-numbers': 'false', 'include-footnotes': 'false', 'include-footnote-body': 'false', 'include-short-copyright': 'false', 'include-passage-horizontal-lines': 'false', 'include-heading-horizontal-lines': 'false', 'include-headings': 'false', 'include-selahs': 'false', 'indent-paragraphs': '0', 'indent-poetry': 'false', 'indent-poetry-lines': '0', 'indent-declares': '0', 'indent-psalm-doxology': '0' } # HTTP request headers API_HEADERS = { 'Accept': 'application/json', 'Authorization': 'Token ' + API_KEY, } # Directory for this workflow's cache data CACHEDIR = os.getenv('alfred_workflow_cache') CACHE_MAXAGE = 86400 * 40 # 40 days def log(s, *args): """Write message to Alfred's debugger. Args: s (basestring): Simple string or sprintf-style format. *args: Arguments to format string. """ if args: s = s % args print(s, file=sys.stderr) class ESVError(Exception): """Base error class.""" class NotFound(ESVError): """Raised if no passage was found.""" def __str__(self): """Error message.""" return 'No passage found' class APIError(ESVError): """Raised if API call fails.""" class Cache(object): """Cache results of API queries. Attributes: dirpath (str): Path to cache directory. """ def __init__(self, dirpath): """Create a new cache. Args: dirpath (str): Directory to store cache files. """ log('cache directory=%s', dirpath) self.dirpath = dirpath if dirpath is None: # not being run from Alfred return # Alfred doesn't create the directory for you... if not os.path.exists(dirpath): os.makedirs(dirpath) else: # remove old cache files self.clean() def search(self, query): """Perform API query, using cached results if not expired. Args: query (unicode): Search string. Returns: Passage: Passage from API or cache. """ cachepath = None if self.dirpath: cachepath = os.path.join( self.dirpath, md5(query.encode('utf-8')).hexdigest() + '.json' ) # ------------------------------------------------------ # Try to load data from cache if cachepath and os.path.exists(cachepath): # Expired cache files were deleted when `Cache` was created log('[cache] loading passage for "%s" from cache ...', query) with open(cachepath) as fp: data = json.load(fp) return Passage.from_response(data) # ------------------------------------------------------ # Fetch data from API # Combine query and options into GET parameters params = dict(q=query.encode('utf-8')) params.update(API_OPTIONS) # Execute request data = fetch_url(API_URL, params, API_HEADERS) passage = Passage.from_response(data) if cachepath: # Cache response with open(cachepath, 'wb') as fp: json.dump(data, fp) return passage def clean(self): """Remove expired cache files.""" i = 0 for fn in os.listdir(self.dirpath): p = os.path.join(self.dirpath, fn) if time() - os.stat(p).st_mtime > CACHE_MAXAGE: os.unlink(p) i += 1 if i: log('[cache] deleted %d stale cache file(s)', i) class Passage(object): """A Bible passage. Attributes: fulltext (unicode): Passage text as paragraphs ref (unicode): Canonical passage reference summary (unicode): Passage text on one line with_ref (unicode): Passage as paragraphs + reference """ @classmethod def from_response(cls, data): """Create a `Passage` from API response. Args: data (dict): Decoded JSON API response. Returns: Passage: Passage parsed from API response. Raises: NotFound: Raised if ``data`` contains no passage(s). """ if not data.get('canonical') or not data.get('passages'): raise NotFound() ref = data['canonical'] s = data['passages'][0] summary = re.sub(r'\s+', ' ', s).strip() p = cls(ref, summary, s) log('---------- passage -----------') log('%s', p) log('---------- /passage ----------') return p def __init__(self, ref=u'', summary=u'', fulltext=u''): """Create new `Passage`.""" self.ref = ref self.summary = summary self.fulltext = fulltext self.with_ref = u'{}\n\n({} ESV)'.format(fulltext.rstrip(), ref) def __str__(self): """Passage as formatted bytestring. Returns: str: Full text of passage with reference. """ return self.__unicode__().encode('utf-8') def __unicode__(self): """Passage as formatted Unicode string. Returns: unicode: Full text of passage with reference. """ return self.with_ref @property def item(self): """Alfred item `dict`. Returns: dict: Alfred item for JSON serialisation. """ return { 'title': self.ref, 'subtitle': self.summary, 'autocomplete': self.ref, 'arg': self.with_ref, 'valid': True, 'text': { 'largetype': self.with_ref, 'copytext': self.with_ref, }, } def fetch_url(url, params, headers): """Fetch a URL using cURL and parse response as JSON. Args: url (str): Base URL without GET parameters. params (dict): GET parameters. headers (dict): HTTP headers. Returns: object: Deserialised HTTP JSON response. Raises: APIError: Raised if API returns an error. """ # Encode GET parameters and add to URL qs = urlencode(params) url = url + '?' + qs # Build cURL command cmd = ['/usr/bin/curl', '-sSL', url] for k, v in headers.items(): cmd.extend(['-H', '{}: {}'.format(k, v)]) # Run command and parse response output = subprocess.check_output(cmd) log('---------- response -----------') log('%r', output) log('---------- /response ----------') data = json.loads(output) if 'detail' in data: # 'detail' contains any API error message raise APIError(data['detail']) return data def exit_with_error(title, err, tb=False): """Show an error message in Alfred and exit script. Args: title (unicode): Title of Alfred item. err (Exception): Error whose message to show as item subtitle. tb (bool, optional): If `True`, show a full traceback in Alfred's debugger. """ # Log to debugger if tb: import traceback log(traceback.format_exc()) else: log('ERROR: %s', err) # Send error message to Alfred output = { 'items': [{'title': title, 'subtitle': str(err)}] } json.dump(output, sys.stdout) sys.exit(1) # 1 indicates something went wrong def main(): """Run Script Filter.""" log('.') # Ensure real log starts on a new line # Fetch user query and decode to Unicode query = sys.argv[1].decode('utf-8') cache = Cache(CACHEDIR) try: passage = cache.search(query) except ESVError as err: exit_with_error(query, err, False) except Exception as err: exit_with_error(query, err, True) # Show passage in Alfred json.dump({'items': [passage.item]}, sys.stdout) if __name__ == '__main__': start = time() main() log('------ %0.3fs ------', time() - start)  
  13. Like
    emmanuel reacted to deanishe in ESV Online Bible   
    Then get clicking on that heart-type thing on my posts. I've got days to win  
  14. Like
    emmanuel reacted to milopus in ESV Online Bible   
    thank you sooooo much.  I've been waiting for this update.  This is a huge time saver for me and my whole family.
     
  15. Thanks
    emmanuel got a reaction from milopus in ESV Online Bible   
    Thank you for your patience! My auth key was finally approved, and the workflow on Packal has been updated with the fix.
×
×
  • Create New...