pax_global_header00006660000000000000000000000064126671315460014525gustar00rootroot0000000000000052 comment=772a5f4db3707ea0253691d930bf648d1344913a gntp-1.0.3/000077500000000000000000000000001266713154600124765ustar00rootroot00000000000000gntp-1.0.3/.gitignore000066400000000000000000000001531266713154600144650ustar00rootroot00000000000000*.pyc dist/* build/* MANIFEST README.html *.egg-info/* # Testing files .tox/* .coverage cover/* htmlcov/* gntp-1.0.3/LICENSE000066400000000000000000000020401266713154600134770ustar00rootroot00000000000000Copyright (c) 2013 Paul Traylor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gntp-1.0.3/MANIFEST.in000066400000000000000000000000231266713154600142270ustar00rootroot00000000000000include README.rst gntp-1.0.3/Makefile000066400000000000000000000107501266713154600141410ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) docs .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GNTP.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GNTP.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/GNTP" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GNTP" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." gntp-1.0.3/README.rst000066400000000000000000000071601266713154600141710ustar00rootroot00000000000000GNTP ==== This is a Python library for working with the `Growl Notification Transport Protocol `_ It should work as a dropin replacement for the older Python bindings Installation ------------ You can install with pip :: $ pip install gntp then test the module :: $ python -m gntp.notifier Simple Usage ------------ :: # GNTP uses the standard Python logging import logging logging.basicConfig(level=logging.INFO) import gntp.notifier # Simple "fire and forget" notification gntp.notifier.mini("Here's a quick message") # More complete example growl = gntp.notifier.GrowlNotifier( applicationName = "My Application Name", notifications = ["New Updates","New Messages"], defaultNotifications = ["New Messages"], # hostname = "computer.example.com", # Defaults to localhost # password = "abc123" # Defaults to a blank password ) growl.register() # Send one message growl.notify( noteType = "New Messages", title = "You have a new message", description = "A longer message description", icon = "http://example.com/icon.png", sticky = False, priority = 1, ) # Try to send a different type of message # This one may fail since it is not in our list # of defaultNotifications growl.notify( noteType = "New Updates", title = "There is a new update to download", description = "A longer message description", icon = "http://example.com/icon.png", sticky = False, priority = -1, ) URL based images do not work in the OSX version of `growl `_ 1.4 You can send the image along with the notification to get around this. :: image = open('/path/to/image.png', 'rb').read() growl.notify( noteType = "New Messages", title = "You have a new message", description = "This time we embed the image", icon = image, ) .. note:: With Growl 2 and above user can choose to pass notification to system wide notifications center. In this case ``icon`` argument would be ignored by the notification center (there would always be Growl icon instead). Bugs ---- `GitHub issue tracker `_ Changelog --------- `v1.0.3 `_ - Allow file:// scheme to be used for icons `v1.0.2 `_ - Fix bug with incoming password hash - Added info about license in each source file `v1.0.1 `_ - Fix bug with binary data (images) being encoded incorrectly `v1.0 `_ - Python 3.3 Support `v0.9 `_ - Remove duplicate code from gntp.config - Catch all errors and rethrow them as gntp.errors to make it easier for other programs to deal with errors from the gntp library. - Ensure that we open resource files as "rb" and update the documentation `v0.8 `_ - Fix a bug where resource sections were missing a CRLF - Fix a bug where the cli client was using config values over options - Add support for coalescing `v0.7 `_ - Support for images - Better test coverage support `0.6 `_ - ConfigParser aware GrowlNotifier that reads settings from ~/.gntp gntp-1.0.3/docs/000077500000000000000000000000001266713154600134265ustar00rootroot00000000000000gntp-1.0.3/docs/_templates/000077500000000000000000000000001266713154600155635ustar00rootroot00000000000000gntp-1.0.3/docs/_templates/sidebarlogo.html000066400000000000000000000003661266713154600207500ustar00rootroot00000000000000

Useful Links

gntp-1.0.3/docs/conf.py000066400000000000000000000161041266713154600147270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # GNTP documentation build configuration file, created by # sphinx-quickstart on Tue Aug 30 22:54:03 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os sys.path.insert(0, os.path.abspath('..')) from gntp.version import __version__ as gntpversion # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Growl Notification Transport Protocol Python Bindings' copyright = u'2011, Paul Traylor' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = gntpversion # The full version, including alpha/beta/rc tags. release = gntpversion # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'GNTPdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'GNTP.tex', u'GNTP Documentation', u'Paul Traylor', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'gntp', u'GNTP Documentation', [u'Paul Traylor'], 1) ] gntp-1.0.3/docs/core.rst000066400000000000000000000013771266713154600151200ustar00rootroot00000000000000Core GNTP Classes ================= Lower level classes for those who want more control in sending messages Exceptions ---------- .. autoexception:: gntp.errors.AuthError .. autoexception:: gntp.errors.ParseError .. autoexception:: gntp.errors.UnsupportedError GNTP Messages ------------- Classes representing each of the GNTP message types .. autoclass:: gntp.core.GNTPRegister :members: :inherited-members: .. autoclass:: gntp.core.GNTPNotice :members: :inherited-members: .. autoclass:: gntp.core.GNTPSubscribe :members: :inherited-members: .. autoclass:: gntp.core.GNTPOK :members: :inherited-members: .. autoclass:: gntp.core.GNTPError :members: :inherited-members: Helper Functions ---------------- .. autofunction:: gntp.core.parse_gntp gntp-1.0.3/docs/index.rst000066400000000000000000000043061266713154600152720ustar00rootroot00000000000000.. toctree:: :maxdepth: 2 :hidden: core GNTP Basics =========== Python bindings for the `Growl Notification Transport Protocol `_ Bugs can be reported at the `GitHub issue tracker `_ Simple Message Sending ---------------------- :: from gntp.notifier import mini # Send a simple growl message with mostly default values mini("Here's a quick message", callback="http://github.com/") .. autofunction:: gntp.notifier.mini Detailed Message Sending ------------------------ .. autoclass:: gntp.notifier.GrowlNotifier The GrowlNotifier class is intended to mostly mirror the older python bindings for growl .. automethod:: gntp.notifier.GrowlNotifier.register .. automethod:: gntp.notifier.GrowlNotifier.notify .. automethod:: gntp.notifier.GrowlNotifier.subscribe Complete Example ---------------- :: import gntp.notifier # Simple "fire and forget" notification gntp.notifier.mini("Here's a quick message") # More complete example growl = gntp.notifier.GrowlNotifier( applicationName = "My Application Name", notifications = ["New Updates","New Messages"], defaultNotifications = ["New Messages"], # hostname = "computer.example.com", # Defaults to localhost # password = "abc123" # Defaults to a blank password ) growl.register() # Send one message growl.notify( noteType = "New Messages", title = "You have a new message", description = "A longer message description", icon = "http://example.com/icon.png", sticky = False, priority = 1, ) # Try to send a different type of message # This one may fail since it is not in our list # of defaultNotifications growl.notify( noteType = "New Updates", title = "There is a new update to download", description = "A longer message description", icon = "http://example.com/icon.png", sticky = False, priority = -1, ) # Send the image with the growl notification image = open('/path/to/icon.png', 'rb').read() growl.notify( noteType = "New Messages", title = "Now with icons", description = "This time we attach the image", icon = image, ) GNTP Configfile Example ----------------------- .. autoclass:: gntp.config.GrowlNotifier gntp-1.0.3/fabfile.py000066400000000000000000000010161266713154600144360ustar00rootroot00000000000000from fabric.api import local def clean(): "Remove all generated files" local("rm -rf build dist") local("find . -name *.pyc -delete") def test(): "Run unittests" local('nosetests -v --with-coverage') local('coverage html') def html(): "Generate Sphinx documentation" local("sphinx-build -b html -d build/doctrees docs build/html") def build(): local("python setup.py build") def install(): local("python setup.py install") def upload_docs(): local("python setup.py upload_docs --upload-dir build/html") gntp-1.0.3/gntp/000077500000000000000000000000001266713154600134465ustar00rootroot00000000000000gntp-1.0.3/gntp/__init__.py000066400000000000000000000000001266713154600155450ustar00rootroot00000000000000gntp-1.0.3/gntp/cli.py000066400000000000000000000100551266713154600145700ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE import logging import os import sys from optparse import OptionParser, OptionGroup from gntp.notifier import GrowlNotifier from gntp.shim import RawConfigParser from gntp.version import __version__ DEFAULT_CONFIG = os.path.expanduser('~/.gntp') config = RawConfigParser({ 'hostname': 'localhost', 'password': None, 'port': 23053, }) config.read([DEFAULT_CONFIG]) if not config.has_section('gntp'): config.add_section('gntp') class ClientParser(OptionParser): def __init__(self): OptionParser.__init__(self, version="%%prog %s" % __version__) group = OptionGroup(self, "Network Options") group.add_option("-H", "--host", dest="host", default=config.get('gntp', 'hostname'), help="Specify a hostname to which to send a remote notification. [%default]") group.add_option("--port", dest="port", default=config.getint('gntp', 'port'), type="int", help="port to listen on [%default]") group.add_option("-P", "--password", dest='password', default=config.get('gntp', 'password'), help="Network password") self.add_option_group(group) group = OptionGroup(self, "Notification Options") group.add_option("-n", "--name", dest="app", default='Python GNTP Test Client', help="Set the name of the application [%default]") group.add_option("-s", "--sticky", dest='sticky', default=False, action="store_true", help="Make the notification sticky [%default]") group.add_option("--image", dest="icon", default=None, help="Icon for notification (URL or /path/to/file)") group.add_option("-m", "--message", dest="message", default=None, help="Sets the message instead of using stdin") group.add_option("-p", "--priority", dest="priority", default=0, type="int", help="-2 to 2 [%default]") group.add_option("-d", "--identifier", dest="identifier", help="Identifier for coalescing") group.add_option("-t", "--title", dest="title", default=None, help="Set the title of the notification [%default]") group.add_option("-N", "--notification", dest="name", default='Notification', help="Set the notification name [%default]") group.add_option("--callback", dest="callback", help="URL callback") self.add_option_group(group) # Extra Options self.add_option('-v', '--verbose', dest='verbose', default=0, action='count', help="Verbosity levels") def parse_args(self, args=None, values=None): values, args = OptionParser.parse_args(self, args, values) if values.message is None: print('Enter a message followed by Ctrl-D') try: message = sys.stdin.read() except KeyboardInterrupt: exit() else: message = values.message if values.title is None: values.title = ' '.join(args) # If we still have an empty title, use the # first bit of the message as the title if values.title == '': values.title = message[:20] values.verbose = logging.WARNING - values.verbose * 10 return values, message def main(): (options, message) = ClientParser().parse_args() logging.basicConfig(level=options.verbose) if not os.path.exists(DEFAULT_CONFIG): logging.info('No config read found at %s', DEFAULT_CONFIG) growl = GrowlNotifier( applicationName=options.app, notifications=[options.name], defaultNotifications=[options.name], hostname=options.host, password=options.password, port=options.port, ) result = growl.register() if result is not True: exit(result) # This would likely be better placed within the growl notifier # class but until I make _checkIcon smarter this is "easier" if options.icon and growl._checkIcon(options.icon) is False: logging.info('Loading image %s', options.icon) f = open(options.icon, 'rb') options.icon = f.read() f.close() result = growl.notify( noteType=options.name, title=options.title, description=message, icon=options.icon, sticky=options.sticky, priority=options.priority, callback=options.callback, identifier=options.identifier, ) if result is not True: exit(result) if __name__ == "__main__": main() gntp-1.0.3/gntp/config.py000066400000000000000000000041751266713154600152740ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ The gntp.config module is provided as an extended GrowlNotifier object that takes advantage of the ConfigParser module to allow us to setup some default values (such as hostname, password, and port) in a more global way to be shared among programs using gntp """ import logging import os import gntp.notifier import gntp.shim __all__ = [ 'mini', 'GrowlNotifier' ] logger = logging.getLogger(__name__) class GrowlNotifier(gntp.notifier.GrowlNotifier): """ ConfigParser enhanced GrowlNotifier object For right now, we are only interested in letting users overide certain values from ~/.gntp :: [gntp] hostname = ? password = ? port = ? """ def __init__(self, *args, **kwargs): config = gntp.shim.RawConfigParser({ 'hostname': kwargs.get('hostname', 'localhost'), 'password': kwargs.get('password'), 'port': kwargs.get('port', 23053), }) config.read([os.path.expanduser('~/.gntp')]) # If the file does not exist, then there will be no gntp section defined # and the config.get() lines below will get confused. Since we are not # saving the config, it should be safe to just add it here so the # code below doesn't complain if not config.has_section('gntp'): logger.info('Error reading ~/.gntp config file') config.add_section('gntp') kwargs['password'] = config.get('gntp', 'password') kwargs['hostname'] = config.get('gntp', 'hostname') kwargs['port'] = config.getint('gntp', 'port') super(GrowlNotifier, self).__init__(*args, **kwargs) def mini(description, **kwargs): """Single notification function Simple notification function in one line. Has only one required parameter and attempts to use reasonable defaults for everything else :param string description: Notification message """ kwargs['notifierFactory'] = GrowlNotifier gntp.notifier.mini(description, **kwargs) if __name__ == '__main__': # If we're running this module directly we're likely running it as a test # so extra debugging is useful logging.basicConfig(level=logging.INFO) mini('Testing mini notification') gntp-1.0.3/gntp/core.py000066400000000000000000000336561266713154600147650ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE import hashlib import re import time import gntp.shim import gntp.errors as errors __all__ = [ 'GNTPRegister', 'GNTPNotice', 'GNTPSubscribe', 'GNTPOK', 'GNTPError', 'parse_gntp', ] #GNTP/ [:][ :.] GNTP_INFO_LINE = re.compile( 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' + ' (?P[A-Z0-9]+(:(?P[A-F0-9]+))?) ?' + '((?P[A-Z0-9]+):(?P[A-F0-9]+).(?P[A-F0-9]+))?\r\n', re.IGNORECASE ) GNTP_INFO_LINE_SHORT = re.compile( 'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)', re.IGNORECASE ) GNTP_HEADER = re.compile('([\w-]+):(.+)') GNTP_EOL = gntp.shim.b('\r\n') GNTP_SEP = gntp.shim.b(': ') class _GNTPBuffer(gntp.shim.StringIO): """GNTP Buffer class""" def writeln(self, value=None): if value: self.write(gntp.shim.b(value)) self.write(GNTP_EOL) def writeheader(self, key, value): if not isinstance(value, str): value = str(value) self.write(gntp.shim.b(key)) self.write(GNTP_SEP) self.write(gntp.shim.b(value)) self.write(GNTP_EOL) class _GNTPBase(object): """Base initilization :param string messagetype: GNTP Message type :param string version: GNTP Protocol version :param string encription: Encryption protocol """ def __init__(self, messagetype=None, version='1.0', encryption=None): self.info = { 'version': version, 'messagetype': messagetype, 'encryptionAlgorithmID': encryption } self.hash_algo = { 'MD5': hashlib.md5, 'SHA1': hashlib.sha1, 'SHA256': hashlib.sha256, 'SHA512': hashlib.sha512, } self.headers = {} self.resources = {} # For Python2 we can just return the bytes as is without worry # but on Python3 we want to make sure we return the packet as # a unicode string so that things like logging won't get confused if gntp.shim.PY2: def __str__(self): return self.encode() else: def __str__(self): return gntp.shim.u(self.encode()) def _parse_info(self, data): """Parse the first line of a GNTP message to get security and other info values :param string data: GNTP Message :return dict: Parsed GNTP Info line """ match = GNTP_INFO_LINE.match(data) if not match: raise errors.ParseError('ERROR_PARSING_INFO_LINE') info = match.groupdict() if info['encryptionAlgorithmID'] == 'NONE': info['encryptionAlgorithmID'] = None return info def set_password(self, password, encryptAlgo='MD5'): """Set a password for a GNTP Message :param string password: Null to clear password :param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512 """ if not password: self.info['encryptionAlgorithmID'] = None self.info['keyHashAlgorithm'] = None return self.password = gntp.shim.b(password) self.encryptAlgo = encryptAlgo.upper() if not self.encryptAlgo in self.hash_algo: raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo) hashfunction = self.hash_algo.get(self.encryptAlgo) password = password.encode('utf8') seed = time.ctime().encode('utf8') salt = hashfunction(seed).hexdigest() saltHash = hashfunction(seed).digest() keyBasis = password + saltHash key = hashfunction(keyBasis).digest() keyHash = hashfunction(key).hexdigest() self.info['keyHashAlgorithmID'] = self.encryptAlgo self.info['keyHash'] = keyHash.upper() self.info['salt'] = salt.upper() def _decode_hex(self, value): """Helper function to decode hex string to `proper` hex string :param string value: Human readable hex string :return string: Hex string """ result = '' for i in range(0, len(value), 2): tmp = int(value[i:i + 2], 16) result += chr(tmp) return result def _decode_binary(self, rawIdentifier, identifier): rawIdentifier += '\r\n\r\n' dataLength = int(identifier['Length']) pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier) pointerEnd = pointerStart + dataLength data = self.raw[pointerStart:pointerEnd] if not len(data) == dataLength: raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data))) return data def _validate_password(self, password): """Validate GNTP Message against stored password""" self.password = password if password is None: raise errors.AuthError('Missing password') keyHash = self.info.get('keyHash', None) if keyHash is None and self.password is None: return True if keyHash is None: raise errors.AuthError('Invalid keyHash') if self.password is None: raise errors.AuthError('Missing password') keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5') password = self.password.encode('utf8') saltHash = self._decode_hex(self.info['salt']) keyBasis = password + saltHash self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest() keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest() if not keyHash.upper() == self.info['keyHash'].upper(): raise errors.AuthError('Invalid Hash') return True def validate(self): """Verify required headers""" for header in self._requiredHeaders: if not self.headers.get(header, False): raise errors.ParseError('Missing Notification Header: ' + header) def _format_info(self): """Generate info line for GNTP Message :return string: """ info = 'GNTP/%s %s' % ( self.info.get('version'), self.info.get('messagetype'), ) if self.info.get('encryptionAlgorithmID', None): info += ' %s:%s' % ( self.info.get('encryptionAlgorithmID'), self.info.get('ivValue'), ) else: info += ' NONE' if self.info.get('keyHashAlgorithmID', None): info += ' %s:%s.%s' % ( self.info.get('keyHashAlgorithmID'), self.info.get('keyHash'), self.info.get('salt') ) return info def _parse_dict(self, data): """Helper function to parse blocks of GNTP headers into a dictionary :param string data: :return dict: Dictionary of parsed GNTP Headers """ d = {} for line in data.split('\r\n'): match = GNTP_HEADER.match(line) if not match: continue key = match.group(1).strip() val = match.group(2).strip() d[key] = val return d def add_header(self, key, value): self.headers[key] = value def add_resource(self, data): """Add binary resource :param string data: Binary Data """ data = gntp.shim.b(data) identifier = hashlib.md5(data).hexdigest() self.resources[identifier] = data return 'x-growl-resource://%s' % identifier def decode(self, data, password=None): """Decode GNTP Message :param string data: """ self.password = password self.raw = gntp.shim.u(data) parts = self.raw.split('\r\n\r\n') self.info = self._parse_info(self.raw) self.headers = self._parse_dict(parts[0]) def encode(self): """Encode a generic GNTP Message :return string: GNTP Message ready to be sent. Returned as a byte string """ buff = _GNTPBuffer() buff.writeln(self._format_info()) #Headers for k, v in self.headers.items(): buff.writeheader(k, v) buff.writeln() #Resources for resource, data in self.resources.items(): buff.writeheader('Identifier', resource) buff.writeheader('Length', len(data)) buff.writeln() buff.write(data) buff.writeln() buff.writeln() return buff.getvalue() class GNTPRegister(_GNTPBase): """Represents a GNTP Registration Command :param string data: (Optional) See decode() :param string password: (Optional) Password to use while encoding/decoding messages """ _requiredHeaders = [ 'Application-Name', 'Notifications-Count' ] _requiredNotificationHeaders = ['Notification-Name'] def __init__(self, data=None, password=None): _GNTPBase.__init__(self, 'REGISTER') self.notifications = [] if data: self.decode(data, password) else: self.set_password(password) self.add_header('Application-Name', 'pygntp') self.add_header('Notifications-Count', 0) def validate(self): '''Validate required headers and validate notification headers''' for header in self._requiredHeaders: if not self.headers.get(header, False): raise errors.ParseError('Missing Registration Header: ' + header) for notice in self.notifications: for header in self._requiredNotificationHeaders: if not notice.get(header, False): raise errors.ParseError('Missing Notification Header: ' + header) def decode(self, data, password): """Decode existing GNTP Registration message :param string data: Message to decode """ self.raw = gntp.shim.u(data) parts = self.raw.split('\r\n\r\n') self.info = self._parse_info(self.raw) self._validate_password(password) self.headers = self._parse_dict(parts[0]) for i, part in enumerate(parts): if i == 0: continue # Skip Header if part.strip() == '': continue notice = self._parse_dict(part) if notice.get('Notification-Name', False): self.notifications.append(notice) elif notice.get('Identifier', False): notice['Data'] = self._decode_binary(part, notice) #open('register.png','wblol').write(notice['Data']) self.resources[notice.get('Identifier')] = notice def add_notification(self, name, enabled=True): """Add new Notification to Registration message :param string name: Notification Name :param boolean enabled: Enable this notification by default """ notice = {} notice['Notification-Name'] = name notice['Notification-Enabled'] = enabled self.notifications.append(notice) self.add_header('Notifications-Count', len(self.notifications)) def encode(self): """Encode a GNTP Registration Message :return string: Encoded GNTP Registration message. Returned as a byte string """ buff = _GNTPBuffer() buff.writeln(self._format_info()) #Headers for k, v in self.headers.items(): buff.writeheader(k, v) buff.writeln() #Notifications if len(self.notifications) > 0: for notice in self.notifications: for k, v in notice.items(): buff.writeheader(k, v) buff.writeln() #Resources for resource, data in self.resources.items(): buff.writeheader('Identifier', resource) buff.writeheader('Length', len(data)) buff.writeln() buff.write(data) buff.writeln() buff.writeln() return buff.getvalue() class GNTPNotice(_GNTPBase): """Represents a GNTP Notification Command :param string data: (Optional) See decode() :param string app: (Optional) Set Application-Name :param string name: (Optional) Set Notification-Name :param string title: (Optional) Set Notification Title :param string password: (Optional) Password to use while encoding/decoding messages """ _requiredHeaders = [ 'Application-Name', 'Notification-Name', 'Notification-Title' ] def __init__(self, data=None, app=None, name=None, title=None, password=None): _GNTPBase.__init__(self, 'NOTIFY') if data: self.decode(data, password) else: self.set_password(password) if app: self.add_header('Application-Name', app) if name: self.add_header('Notification-Name', name) if title: self.add_header('Notification-Title', title) def decode(self, data, password): """Decode existing GNTP Notification message :param string data: Message to decode. """ self.raw = gntp.shim.u(data) parts = self.raw.split('\r\n\r\n') self.info = self._parse_info(self.raw) self._validate_password(password) self.headers = self._parse_dict(parts[0]) for i, part in enumerate(parts): if i == 0: continue # Skip Header if part.strip() == '': continue notice = self._parse_dict(part) if notice.get('Identifier', False): notice['Data'] = self._decode_binary(part, notice) #open('notice.png','wblol').write(notice['Data']) self.resources[notice.get('Identifier')] = notice class GNTPSubscribe(_GNTPBase): """Represents a GNTP Subscribe Command :param string data: (Optional) See decode() :param string password: (Optional) Password to use while encoding/decoding messages """ _requiredHeaders = [ 'Subscriber-ID', 'Subscriber-Name', ] def __init__(self, data=None, password=None): _GNTPBase.__init__(self, 'SUBSCRIBE') if data: self.decode(data, password) else: self.set_password(password) class GNTPOK(_GNTPBase): """Represents a GNTP OK Response :param string data: (Optional) See _GNTPResponse.decode() :param string action: (Optional) Set type of action the OK Response is for """ _requiredHeaders = ['Response-Action'] def __init__(self, data=None, action=None): _GNTPBase.__init__(self, '-OK') if data: self.decode(data) if action: self.add_header('Response-Action', action) class GNTPError(_GNTPBase): """Represents a GNTP Error response :param string data: (Optional) See _GNTPResponse.decode() :param string errorcode: (Optional) Error code :param string errordesc: (Optional) Error Description """ _requiredHeaders = ['Error-Code', 'Error-Description'] def __init__(self, data=None, errorcode=None, errordesc=None): _GNTPBase.__init__(self, '-ERROR') if data: self.decode(data) if errorcode: self.add_header('Error-Code', errorcode) self.add_header('Error-Description', errordesc) def error(self): return (self.headers.get('Error-Code', None), self.headers.get('Error-Description', None)) def parse_gntp(data, password=None): """Attempt to parse a message as a GNTP message :param string data: Message to be parsed :param string password: Optional password to be used to verify the message """ data = gntp.shim.u(data) match = GNTP_INFO_LINE_SHORT.match(data) if not match: raise errors.ParseError('INVALID_GNTP_INFO') info = match.groupdict() if info['messagetype'] == 'REGISTER': return GNTPRegister(data, password=password) elif info['messagetype'] == 'NOTIFY': return GNTPNotice(data, password=password) elif info['messagetype'] == 'SUBSCRIBE': return GNTPSubscribe(data, password=password) elif info['messagetype'] == '-OK': return GNTPOK(data) elif info['messagetype'] == '-ERROR': return GNTPError(data) raise errors.ParseError('INVALID_GNTP_MESSAGE') gntp-1.0.3/gntp/errors.py000066400000000000000000000010071266713154600153320ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE class BaseError(Exception): pass class ParseError(BaseError): errorcode = 500 errordesc = 'Error parsing the message' class AuthError(BaseError): errorcode = 400 errordesc = 'Error with authorization' class UnsupportedError(BaseError): errorcode = 500 errordesc = 'Currently unsupported by gntp.py' class NetworkError(BaseError): errorcode = 500 errordesc = "Error connecting to growl server" gntp-1.0.3/gntp/notifier.py000066400000000000000000000201611266713154600156370ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ The gntp.notifier module is provided as a simple way to send notifications using GNTP .. note:: This class is intended to mostly mirror the older Python bindings such that you should be able to replace instances of the old bindings with this class. `Original Python bindings `_ """ import logging import platform import socket import sys from gntp.version import __version__ import gntp.core import gntp.errors as errors import gntp.shim __all__ = [ 'mini', 'GrowlNotifier', ] logger = logging.getLogger(__name__) class GrowlNotifier(object): """Helper class to simplfy sending Growl messages :param string applicationName: Sending application name :param list notification: List of valid notifications :param list defaultNotifications: List of notifications that should be enabled by default :param string applicationIcon: Icon URL :param string hostname: Remote host :param integer port: Remote port """ passwordHash = 'MD5' socketTimeout = 3 def __init__(self, applicationName='Python GNTP', notifications=[], defaultNotifications=None, applicationIcon=None, hostname='localhost', password=None, port=23053): self.applicationName = applicationName self.notifications = list(notifications) if defaultNotifications: self.defaultNotifications = list(defaultNotifications) else: self.defaultNotifications = self.notifications self.applicationIcon = applicationIcon self.password = password self.hostname = hostname self.port = int(port) def _checkIcon(self, data): ''' Check the icon to see if it's valid If it's a simple URL icon, then we return True. If it's a data icon then we return False ''' logger.info('Checking icon') return gntp.shim.u(data)[:4] in ['http', 'file'] def register(self): """Send GNTP Registration .. warning:: Before sending notifications to Growl, you need to have sent a registration message at least once """ logger.info('Sending registration to %s:%s', self.hostname, self.port) register = gntp.core.GNTPRegister() register.add_header('Application-Name', self.applicationName) for notification in self.notifications: enabled = notification in self.defaultNotifications register.add_notification(notification, enabled) if self.applicationIcon: if self._checkIcon(self.applicationIcon): register.add_header('Application-Icon', self.applicationIcon) else: resource = register.add_resource(self.applicationIcon) register.add_header('Application-Icon', resource) if self.password: register.set_password(self.password, self.passwordHash) self.add_origin_info(register) self.register_hook(register) return self._send('register', register) def notify(self, noteType, title, description, icon=None, sticky=False, priority=None, callback=None, identifier=None, custom={}): """Send a GNTP notifications .. warning:: Must have registered with growl beforehand or messages will be ignored :param string noteType: One of the notification names registered earlier :param string title: Notification title (usually displayed on the notification) :param string description: The main content of the notification :param string icon: Icon URL path :param boolean sticky: Sticky notification :param integer priority: Message priority level from -2 to 2 :param string callback: URL callback :param dict custom: Custom attributes. Key names should be prefixed with X- according to the spec but this is not enforced by this class .. warning:: For now, only URL callbacks are supported. In the future, the callback argument will also support a function """ logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port) assert noteType in self.notifications notice = gntp.core.GNTPNotice() notice.add_header('Application-Name', self.applicationName) notice.add_header('Notification-Name', noteType) notice.add_header('Notification-Title', title) if self.password: notice.set_password(self.password, self.passwordHash) if sticky: notice.add_header('Notification-Sticky', sticky) if priority: notice.add_header('Notification-Priority', priority) if icon: if self._checkIcon(icon): notice.add_header('Notification-Icon', icon) else: resource = notice.add_resource(icon) notice.add_header('Notification-Icon', resource) if description: notice.add_header('Notification-Text', description) if callback: notice.add_header('Notification-Callback-Target', callback) if identifier: notice.add_header('Notification-Coalescing-ID', identifier) for key in custom: notice.add_header(key, custom[key]) self.add_origin_info(notice) self.notify_hook(notice) return self._send('notify', notice) def subscribe(self, id, name, port): """Send a Subscribe request to a remote machine""" sub = gntp.core.GNTPSubscribe() sub.add_header('Subscriber-ID', id) sub.add_header('Subscriber-Name', name) sub.add_header('Subscriber-Port', port) if self.password: sub.set_password(self.password, self.passwordHash) self.add_origin_info(sub) self.subscribe_hook(sub) return self._send('subscribe', sub) def add_origin_info(self, packet): """Add optional Origin headers to message""" packet.add_header('Origin-Machine-Name', platform.node()) packet.add_header('Origin-Software-Name', 'gntp.py') packet.add_header('Origin-Software-Version', __version__) packet.add_header('Origin-Platform-Name', platform.system()) packet.add_header('Origin-Platform-Version', platform.platform()) def register_hook(self, packet): pass def notify_hook(self, packet): pass def subscribe_hook(self, packet): pass def _send(self, messagetype, packet): """Send the GNTP Packet""" packet.validate() data = packet.encode() logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(self.socketTimeout) try: s.connect((self.hostname, self.port)) s.send(data) recv_data = s.recv(1024) while not recv_data.endswith(gntp.shim.b("\r\n\r\n")): recv_data += s.recv(1024) except socket.error: # Python2.5 and Python3 compatibile exception exc = sys.exc_info()[1] raise errors.NetworkError(exc) response = gntp.core.parse_gntp(recv_data) s.close() logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response) if type(response) == gntp.core.GNTPOK: return True logger.error('Invalid response: %s', response.error()) return response.error() def mini(description, applicationName='PythonMini', noteType="Message", title="Mini Message", applicationIcon=None, hostname='localhost', password=None, port=23053, sticky=False, priority=None, callback=None, notificationIcon=None, identifier=None, notifierFactory=GrowlNotifier): """Single notification function Simple notification function in one line. Has only one required parameter and attempts to use reasonable defaults for everything else :param string description: Notification message .. warning:: For now, only URL callbacks are supported. In the future, the callback argument will also support a function """ try: growl = notifierFactory( applicationName=applicationName, notifications=[noteType], defaultNotifications=[noteType], applicationIcon=applicationIcon, hostname=hostname, password=password, port=port, ) result = growl.register() if result is not True: return result return growl.notify( noteType=noteType, title=title, description=description, icon=notificationIcon, sticky=sticky, priority=priority, callback=callback, identifier=identifier, ) except Exception: # We want the "mini" function to be simple and swallow Exceptions # in order to be less invasive logger.exception("Growl error") if __name__ == '__main__': # If we're running this module directly we're likely running it as a test # so extra debugging is useful logging.basicConfig(level=logging.INFO) mini('Testing mini notification') gntp-1.0.3/gntp/shim.py000066400000000000000000000017021266713154600147600ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ Python2.5 and Python3.3 compatibility shim Heavily inspirted by the "six" library. https://pypi.python.org/pypi/six """ import sys PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 if PY3: def b(s): if isinstance(s, bytes): return s return s.encode('utf8', 'replace') def u(s): if isinstance(s, bytes): return s.decode('utf8', 'replace') return s from io import BytesIO as StringIO from configparser import RawConfigParser else: def b(s): if isinstance(s, unicode): return s.encode('utf8', 'replace') return s def u(s): if isinstance(s, unicode): return s if isinstance(s, int): s = str(s) return unicode(s, "utf8", "replace") from StringIO import StringIO from ConfigParser import RawConfigParser b.__doc__ = "Ensure we have a byte string" u.__doc__ = "Ensure we have a unicode string" gntp-1.0.3/gntp/test/000077500000000000000000000000001266713154600144255ustar00rootroot00000000000000gntp-1.0.3/gntp/test/__init__.py000066400000000000000000000014361266713154600165420ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE import unittest from gntp.config import GrowlNotifier class GNTPTestCase(unittest.TestCase): application = 'GNTP unittest' notification_name = 'Testing' notification = { 'noteType': notification_name, 'title': 'Unittest Title', 'description': 'Unittest Description', } def setUp(self): self.growl = GrowlNotifier(self.application, [self.notification_name]) self.growl.register() def _notify(self, **kargs): for k in self.notification: if not k in kargs: kargs[k] = self.notification[k] return self.growl.notify(**kargs) def assertIsTrue(self, result): """Python 2.5 safe way to assert that the result is true""" return self.assertEqual(result, True) gntp-1.0.3/gntp/test/basic_tests.py000066400000000000000000000043421266713154600173050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE # Simple test to send each priority level import logging logging.basicConfig(level=logging.WARNING) import os import unittest from gntp.test import GNTPTestCase import gntp.config import gntp.core import gntp.errors as errors import gntp.notifier ICON_URL = "https://www.google.com/intl/en_com/images/srpr/logo3w.png" ICON_FILE = os.path.join(os.path.dirname(__file__), "growl-icon.png") CALLBACK_URL = "http://github.com" class BasicTests(GNTPTestCase): def test_mini(self): gntp.notifier.mini('Testing gntp.notifier.mini', applicationName=self.application ) def test_config(self): gntp.config.mini('Testing gntp.config.mini', applicationName=self.application ) def test_priority(self): for priority in [2, 1, 0, -1, -2]: self.assertIsTrue(self._notify( description='Priority %s' % priority, priority=priority )) def test_english(self): self.assertIsTrue(self._notify(description='Hello World')) def test_extra(self): self.assertIsTrue(self._notify(description='allô')) def test_japanese(self): self.assertIsTrue(self._notify(description='おはおう')) def test_sticky(self): self.assertIsTrue(self._notify(sticky=True, description='Sticky Test')) def test_unknown_note(self): self.assertRaises(AssertionError, self._notify, noteType='Unknown') def test_parse_error(self): self.assertRaises(errors.ParseError, gntp.core.parse_gntp, 'Invalid GNTP Packet') def test_url_icon(self): self.assertIsTrue(self._notify( icon=ICON_URL, description='test_url_icon', )) def test_data_icon(self): self.assertIsTrue(self._notify( icon=open(ICON_FILE, 'rb').read(), description='test_data_icon', )) def test_file_icon(self): self.assertIsTrue(self._notify( icon='file://' + os.path.abspath(ICON_FILE), description='test_file_icon', )) def test_callback(self): self.assertIsTrue(self._notify( callback=CALLBACK_URL, description='Testing Callback', )) #def test_subscribe(self): # self.assertTrue(self.growl.subscribe( # id='unittest-id', # name='example.com', # port=5000, # )) if __name__ == '__main__': unittest.main() gntp-1.0.3/gntp/test/growl-icon.png000066400000000000000000000460271266713154600172240ustar00rootroot00000000000000PNG  IHDRog KIDATx^%Gy'xqfdHBe@FO .58횅^^8ObX`MPi4h$MIUuϹ=s;T׿U5{;c@Hı:I; 1z㵛dץs&eVT_/R2)I]ꬤ[G <  p:_"Ў}},uuJ `NJ~>B$_DpW!c! mKH'|iڛ LjZ2s ( - r2nZ:):VWK hA@j{szha!?D0 8 :u_IWq/E"iČ;z̀3 <'1 Tkk<f )[evjC5lOsCZ?LΩn` @DLz ĵA%H#[J;Ɖ dop7)a(6ԳXTZ;@'w\I٨sk:Lɹ}x0dKh@:n8]ksd\H!:>3nIqo|d9KsRS;$lk*@2 #gtw:B~ A!:d4RZ= Vv^'ʋv@*)21–)GʸXvC4;U/{3`^$N`9bHeY`bMI겲L3Y+<Λ씎ةdr/LT;@sG˟mARA#Il+a(6o \z 6nڊc8W|s acjy z{YN={1`b_T[-^2DԒǹ>mYI_WBL TD,wn-xlٝhQ'I$╟o{'.Pfm2%Oṣ=عs7mގ޾ *Y891'1@ .v^;tBM_H]~B6ZJ՚G6>Ν@5ɸ 6N̯G\K&^ 50g.(ӄtkm 8IfrUqk_+^yVH$Ç`izo=-@s;=֜Xa|B`&1[R8,@X6~k>x;l|UD n~EN 3>{N۶(e,}g 8N=9E޶Def@\LYBr!婭3yS;( de`Ў?ۊQ c%)al6l2ԛ1 z1mn|4[uq7~ ]A6L0VF!:!4Ȩ6/sM`}hv͒Ns+_r>>qᱟ H0^apADEd Ů^vv#pl Px<,/~%]}oO|zY_cϽ  &XoCu =)uힲ3 4 INfDQNf[y *vttciꑋZ}i`I4 q(J5{ 72 0@/*7?E;mP50٘ GĉёM2{Kxݷk`W5R!V%4B01.Pv Z[Jô\bF L i`pH1 /C"U{-H269\YX-WOōQ3i" B*%XJepyFj*΀e(6`"VpO^٬i;=G$fuzags^r5ԓ̬yK!wT_>tg뗅pl8e:9Bp@Ts/8d T ex;*hԛF+K{ܲP* qXł0n/ر;.t4e QJe hbU|'3]̸hH=/@fzgkgvGFcyǟ #&&A'q.10B҃ DXEDlQor,A (<a}O煑$cG:rNvw--DpM<7R^&EwfL_Ǽy[;.:%c!g5D|wIXJq QQ,ZZZ {цcbpX7@% {+6٪[1MQ$ZN6b)+z (w@4 y\o<OEX:;c,Axif0HyٸW?((ekD'&!|fǬN XLCɴlۀJ 86#~c劲%p,%נsDT0-t<8T l`ׁMo@e3TmH擐INre:]|yjn,-'Tp( @7Kw/3MfF \/b!8 Ւ/d4`E\;+TIэ1w [psf#J0<RiTt(B ,# ض'z>5(][@ՊXlemZgz^*ql](tPڑ$ƍ I f\}h[A2VjX8{]~j>GRF__{Q,(\tU^~C,@,& n| B0)2 rLej. COD0J% @ypWKO?LH4)sSbP`jQlTUqXw_;;˫ˌqj'?gޏ3ǰ4G3+h1VgΜϥ 47޾ ߵ  @ ɔ: .^h@"JEJ58HC{1"QGlCk%+8@ 4]1Wd +j, Aeuf]@nqwYYS3{o|? _xxo;4f! ð`ZX7?~ .܍[3OUm ;^9\ ԭI2&Xqys)Re "AH ^<8PG(\XIR%Lx-w`d71Q}RrݙKmQZ{̱Լdp~ z~ދ "ՑQ(8t)_ZU5cUn1~b_[&|K[qUxݛrĭᛤTt#kGo#N=7e&@ s3tǀ$D#= "L=PG$>9 #BA˥J<<ŤraV [RP4(ۙDSK Ͼ“G_n3txf[7^@1UY3Y7K$v]?/ާIc49 )de D}iT.;I@vm)j1bb.H?HR`" =uo0|mR z/TAS%9s7 ,-dve'mg$ž4{s M1UP1[.Y+{8z"ec{ &VR>$Iem-I_y}2 L/810Pr]C#TOE]]fL䒳?9rdQsqvYׁ 3I1XznLPdP<_ 5kFI c^O4BR(rknx xR88R)F!P""^1aɏa[ @Iں*.dŅ[zQ\Qye9Hi#A,=Es6_cc0Ou=r5%Bu0Xeg)&/RѤBŕ`dɅ -\}0:B6c/:t[d $$5p$\e+!4"P.(\t]9۳MؕO8&7"w$\ lj37m ϼ뽮[t棶]2Bޗ̓?MF0;J"ovڃc#.zL`ɃqGM;RZ({@]GO9xm(,,BT,Z3 0'*n*a[6 2t$ [-}dT[p[7X,N f2MSU my,{jKȦ_"ȱΜ3BXu2`}c%tw#gpv"p%L/X$w@,$∽ͯ8;kOt]NxR@wL FDXQ5IZ(g o7QD!\3C`JjO }bv+hn -Vfk|Y_|z_q3-3˿F rbG#R"Wp8xCL\cP"pzNrRrL-)pN|L }H" caDv7 B .o"^`x!B?d}c=y_ee3==ϴKV'?\B:@مLW&|\oO&q4c fže<093TK0ug?Su,Yh@0=KRBLeV-(ݾ*1q,  c[ @rfbˀPoHڧ }&=o ~wz#WpB"Ɍ#6e潍5ShR٦(IO0^\b$~f2qSIȍ㕞fc2/5'AwS=a,1tHFvU 4bCm#6(,  kt(tB!y(죏an%F\MlglQen Db? Dq .1uuJX}wG0i: Czo@K?!D̔7`qZ &fdgl|.ߛ.'"@qpCu LC1O]2VފJR#!jY^wE/#F4 K F{Dç!vȢ4Lbvntqee:0$a`P* N% vBW]5]џ?ņLJ(w-F)͈#vGPv:KSjp뫯Ǒ"{k=$CEDI0tFz09@g#\es`-(X$k}S5{QQ//ii 3~1kaP8d-ՙ0iM 7JL$w ' MdX_}Ad砎2FF `0Uɵ˜El2Drl7`[25?:$ˤgEc(*),JL\z0XZ%'3&ҁ@R*GL (?jxHm&n C=pl83=Fܠ_[>u3`i؝J3SkJ#Cp n ~뱐_DĨ @9p %9#7ف=֗TiOkb~%JBARR6hVfhs3qN٥G'PbLp-NVy?VJpj:Ͽv:SMRbV<TfAB|O`lB򮒉voupJ{0? CHz$D8:ۂ,`@)NɺD:֨Y_?D9R 0pz_/`E&Q)ڰ-Bf]1Su#~ _8.ٌ훇0_`$"h#:Ix^k3p>?!9i`RGLqDG4}Q>](\t-)ȵMJDN_4?xxKʘZ3u0 A K3`\Y-twIXRF]Q b]Jzq0$'kR3UDt]8Ug>{+Ku2I"ΨjMU4ex@C  ,y=$Į#lF?T#ŧս7 hvfqK$_Lv 3QD < M/$F49(t ĵ-l-zpUT@͏1:XX zz]\bvYI1@XZT"71aJc>VĮG:s;「 N2@fJ $@H9s8}v b䪫{ V$\T{7ݨ*͸6$i Vx0 E"LMS811 `z{~x} x-$Aבg#8$5yQ 0j ӨZ{3)@JrnC\]c4]eͽf4-us!0"c`.VL;W?.$ F6k#fcH0Ѽw09ˍ!gV;6hDnpzFTُ?I=\=zh#28\rh4Ps5k(8ӈ(Q&β-wPDm\q7zUz!G2Nd?O}c6Hr@UpQ oF(r?z oAWmwm[fJ|A/Ik&y* 4%ht &0!R*Y- H J)8 0좿j*# L0N#'P] RX"fSs*-C]i-V Mڏ8mbrR P("ʐX@8b8(AKT)Tړhfmser p=;ZsR_wuaE;a:t)P`Oa5v3)(B,k:Y Fz!RR%}RodlO0%j1_iũc9aÀMf4QʁkUYfsj̨RR;M?K,RsIHIX^aX YGOz+xp D9V C&$MIw-cHʆmsZ zM>ϟG ux&DkTEf"(uam6=kxz ڂ<`h}DabPU\n%B0Mm6 11THF:}DJ9%z+M hr}`I["@ %첢j–TLRu8 0Jͥ!NA{zGxP-3驘d %%!@?TY\fJ;z1UCg! k@Mwo{͚Or1q&&'ϩ==P㌈g3 _kW*3٧XON41> _R~Ѥ jhdn-clL@}&[SX H8>Cs!Q΀. \0h^ 4gC)i7PS}5iDBZbC,EXnxKt laũcq \u+\BF8_^:4xL!_|x[nCk//sԾqIDaB8xG:m9v(Ϣf #lx=[1z0rNNXEPZ-, DHttä ܲ1 z==@#ATg^hU>J)Vl V!k fOyX\Iܩ?~a4lO!J㭯ĩ2AD~ .,"A@Ъ HVͷ< ҇7t-=@1;qUz]8uy<#طPq:ᘦpYK}' ¸l D A7FggpyģO-p.э9=ă< +-0$Y$ٶEfp@ ,C<| բ${zW.P4 6)*&ʐU %*WxXykqRƘՍ]IIqd&ceFA~@Ԣ$<, y8pYg_ѩ-3z!,h{hgf {˗=w 󜛡aQˆ9ww4,\k8j8d0pγyV$>ilw%xXx+ku Ie A0`I05/q$aiƀP,26E V(ᒣ9'f=d%߅pXO ۚhCZFrD24")UF4mPl7xnq D{.~˗+]3R4]SM/ɣ:ݿ&(pR'zzY7wDZ=ݰW-mީ$|vıK{'[wfJ-BMd"hK*<# W( W`qXYFp@&|`ȅS m}|J09xHe[1&c5 fdpU,dp+ @H~,:78s`qA{`cL E3W򫰲3p .xg?t6w2YB`^%0d+&נH L!Q #& (dMr<Jja.Mp!Ʊ!9`|" KMEN0αNב!Κ73˭ggBsKJgv1W{>wԉ?Í|m\/]Ds䲲h"1pW,b"i.E"MՈXh3JTj䜷 ` /A,9 &}%!j jUf#^5vf6bP&#{b;[d%L.t[/KY m2ϷnĄDֈVϏ@GlX|;ܽk߉(=~iDɰC}Q[nrZzrW-J,2dmkƩ2bY,8~-3%$(#V+ M')L.JD ^KwLj KMdͯ0m71`rN`0V7ZuL/֝ݒɧl5Z+ϜIj|\uRFe #16Esk0@耵TPrb!)-UmI?B#]p` $f$ 5lYJ d_~6L=)( giP>K{yEtKqpk `%H),)mIBMlE ߳?Yc)11'D ^PQ\Eᜑq*p_VPRaHXk;c%<'D:&ӟk#?=<}'+q/|uL~+yx~gQWaQ+ۭP!2M4j.|3KQ_h r3 _O( ̶X6{'i%R#D -|q`"EI-!% !h7w)L*P-ܸ صl7`c 8|KNljQ.Mʼ\;߇[7Nk͝=d?ʞ?W{ت\xnO~?41HsE@-S"\f["Ζ(qQ.9 mDx-.MVMi ;رcZnrۤs fՁ>5>}hg bGaJoM BٹgNX_IɳT]47@`f <3MDij4%GUU/g Or]EQK.{D?7 WP,VLðT-ntu 8nrpp Lu.>y4Ycܵb1>' O56jjKhxוwmH>{~BqԌZ2r 걩s (_txd8ZD\2BZ=tbĮPvSrAW@TK&&#--1~ɂ[mەAIruT:!b(Por`` Q߷/lU<|̙R%Q{N=V R&4`ee5KS ~ +0Wczh;Ulۼ@q3N8A2aR#J.u^P\QtA3y)!|˱ \pnHӴVM:Nq{zeP܌0sf'3ֳ xep#l|)~aFh2I>Z$WK|Z .D,( $wnvupN6&8!WMe)xsl# o&0McUN4`):.% ,:*+un9;c>1&97ZRPJ]p*m\##[ 4xo3啔(c:th;zt/MKR*XL4JRhq))h_JI,FpnK/.{+.xO,:.M`pM$(8 %3ȷN%أMK(I@uNEefԢ'ԱLXi ,4 VA+Sٮ(ʞ!$IXgZ:t)dM6{wnz`-Frx捛vRwL N"LBR~<4nG*%GQatzZgJ$~N.-Y,eaGO1QOc`c\) R&BS.(6*6^zNH{,&B6lSSϓwS;>~#_:V2bǎ}}CUD%mKƭ\2>Yn-m"4eYV*,@}sWRbf!ggO1EJݒs&i=RI$)&i? V )*kH1tiFu-3`־n[\-7wٻp_7M 5-_/.tL$cȈB^d^8BƹG+s 8p oi}ū& <?~=dR>mS+XS`;lqXUuj8]*GXD9G gSjN^S@L)-i@0R`(O\'@_J\X<S]' kWd4p@fCrZSƹLsLˆJ?ӔF^Wڍb)`!$D46Јz2'vewWFtR=;g¬YDץ>;8fgu+O)9]SFs*Ր.8N,{obn4}k (D-1Gh56΁9Jʁ N"X./ogy {E^ڕN勝Ժ%Bx8M0})%sNNi#)$ ԩhH&;JKN 8X./|mum'p"3OZ:a;QqH2)L[Q6U*I/ %ve I/кZkp@0ik)RPhZV,0LIQY:%vS.Ci;s`)I:Y>\S,|Vm>OB[L BF(ƫI8ԓB X݉\$)[5jMM< ke+y%Hje)YWZ%kw\t1TOEi6iff}3gTԧ.5&pv [R˦X O氟}6L5Jdۋ}^>f6ASRsJo9u2E]eѫR\tDiS2JƔsPL(IM*)`XDzگ%n'P6Fe!SʞLOϴܱ+6(0;ރsA˷ebdN@C\ZMMIXڟ#c> QIENDB`gntp-1.0.3/gntp/test/test_config.py000066400000000000000000000016471266713154600173130ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE # Test when we don't have a config file # Since we don't know if the user will have a config file or not when # running this test, we will move it out of the way and then move it back # when we're done import os from gntp.test import GNTPTestCase from gntp.config import GrowlNotifier ORIGINAL_CONFIG = os.path.expanduser('~/.gntp') BACKUP_CONFIG = os.path.expanduser('~/.gntp.backup') class ConfigTests(GNTPTestCase): def setUp(self): if os.path.exists(ORIGINAL_CONFIG): os.rename(ORIGINAL_CONFIG, BACKUP_CONFIG) self.growl = GrowlNotifier(self.application, [self.notification_name]) self.growl.register() def test_missing_config(self): self.assertIsTrue(self._notify(description='No config file test')) def tearDown(self): if os.path.exists(BACKUP_CONFIG): os.rename(BACKUP_CONFIG, ORIGINAL_CONFIG) gntp-1.0.3/gntp/test/test_custom.py000066400000000000000000000006601266713154600173520ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ Test sending custom attributes http://www.growlforwindows.com/gfw/help/gntp.aspx#custom """ from gntp.test import GNTPTestCase class TestCustom(GNTPTestCase): def test_custom_values(self): self._notify(title='Custom Attributes', custom={ 'X-Custom1': 'Some Value', 'X-Custom2': 'Some other value', }) gntp-1.0.3/gntp/test/test_errors.py000066400000000000000000000013641266713154600173560ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ Test the various error condtions This test runs with the gntp.config module so that we can get away without having to hardcode our password in a test script. Please fill out your ~/.gntp config before running """ from gntp.test import GNTPTestCase import gntp.errors as errors class TestErrors(GNTPTestCase): def test_connection_error(self): #self.growl.hostname = '0.0.0.0' # Port 9 would be the discard protocol. We just want a "null port" # for testing # http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers self.growl.port = 9 self.assertRaises(errors.NetworkError, self._notify, description='Connection Error') gntp-1.0.3/gntp/test/test_hash.py000066400000000000000000000023231266713154600167610ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE """ Test the various hashing methods This test runs with the gntp.config module so that we can get away without having to hardcode our password in a test script. Please fill out your ~/.gntp config before running """ import os from gntp.test import GNTPTestCase from gntp.errors import UnsupportedError class TestHash(GNTPTestCase): def _hash(self, hashName): self.growl.passwordHash = hashName return self._notify(description=hashName) def test_config(self): """Test to see if our config file exists If our config file doesn't exist, then we have no password to test with, so our password hash is no good """ config = os.path.expanduser('~/.gntp') self.assertIsTrue(os.path.exists(config)) def test_md5(self): self.assertIsTrue(self._hash('MD5')) def test_sha1(self): self.assertIsTrue(self._hash('SHA1')) def test_sha256(self): self.assertIsTrue(self._hash('SHA256')) def test_sha512(self): self.assertIsTrue(self._hash('SHA512')) def test_fake(self): '''Fake hash should not work''' self.growl.password = 'foobar' self.assertRaises(UnsupportedError, self._hash, 'fake-hash') gntp-1.0.3/gntp/test/test_resources.py000066400000000000000000000023231266713154600200500ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE # https://github.com/kfdm/gntp/issues/27 # nosetests -v gntp.test.test_resources:ResourceTest import os from gntp.test import GNTPTestCase import gntp.core ICON_FILE = os.path.join(os.path.dirname(__file__), "growl-icon.png") ICON_DATA = open(ICON_FILE, 'rb').read() FILE_DATA = open(__file__).read() class ResourceTest(GNTPTestCase): def test_single_resource(self): notification = gntp.core.GNTPNotice( app=self.application, name=self.notification_name, title="Testing Single Resource", password=self.growl.password, ) resource = notification.add_resource(ICON_DATA) notification.add_header('Notification-Icon', resource) self.assertIsTrue(self.growl._send('notify', notification)) def test_double_resource(self): notification = gntp.core.GNTPNotice( app=self.application, name=self.notification_name, title="Testing Double Resource", password=self.growl.password, ) notification.add_resource(FILE_DATA) resource = notification.add_resource(ICON_DATA) notification.add_header('Notification-Icon', resource) self.assertIsTrue(self.growl._send('notify', notification)) gntp-1.0.3/gntp/version.py000066400000000000000000000002031266713154600155000ustar00rootroot00000000000000# Copyright: 2013 Paul Traylor # These sources are released under the terms of the MIT license: see LICENSE __version__ = '1.0.3' gntp-1.0.3/pylintrc000066400000000000000000000000401266713154600142570ustar00rootroot00000000000000[MESSAGE_CONTROL] disable=W0312 gntp-1.0.3/scripts/000077500000000000000000000000001266713154600141655ustar00rootroot00000000000000gntp-1.0.3/scripts/gntp000066400000000000000000000000511266713154600150540ustar00rootroot00000000000000#!python import gntp.cli gntp.cli.main() gntp-1.0.3/setup.py000066400000000000000000000021071266713154600142100ustar00rootroot00000000000000try: from setuptools import setup kwargs = { 'entry_points':{ 'console_scripts': [ 'gntp = gntp.cli:main' ] } } except ImportError: from distutils.core import setup kwargs = { 'scripts':['scripts/gntp'] } from gntp.version import __version__ setup( name='gntp', description='Growl Notification Transport Protocol for Python', long_description=open('README.rst').read(), author='Paul Traylor', url='http://github.com/kfdm/gntp/', version=__version__, packages=['gntp'], # http://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: Implementation :: PyPy', ], **kwargs ) gntp-1.0.3/test/000077500000000000000000000000001266713154600134555ustar00rootroot00000000000000gntp-1.0.3/test/__init__.py000066400000000000000000000000001266713154600155540ustar00rootroot00000000000000gntp-1.0.3/test/attributes.py000066400000000000000000000005371266713154600162220ustar00rootroot00000000000000#!/usr/bin/env python # Simple manual test to make sure that attributes do not # accumulate in the base classes # https://github.com/kfdm/gntp/issues/10 import gntp import gntp.notifier a = gntp.notifier.GrowlNotifier(notifications=['A']) b = gntp.notifier.GrowlNotifier(notifications=['B']) a.notify('A','A','A',sticky=True) b.notify('B','B','B') gntp-1.0.3/test/subscribe.py000066400000000000000000000005331266713154600160110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Simple script to test sending UTF8 text with the GrowlNotifier class import logging logging.basicConfig(level=logging.DEBUG) from gntp.notifier import GrowlNotifier import platform growl = GrowlNotifier(notifications=['Testing'],password='password',hostname='ayu') growl.subscribe(platform.node(),platform.node(),12345) gntp-1.0.3/tox.ini000066400000000000000000000002771266713154600140170ustar00rootroot00000000000000# content of: tox.ini , put in same dir as setup.py [tox] envlist = py25,py26,py27,pypy,py35 [testenv] deps=nose commands=nosetests [testenv:py35] deps=nose commands=nosetests --nologcapture