├── tests ├── __init__.py ├── hello.pdf ├── test_with_cups.py ├── test_widget.py ├── test_async_subprocess.py ├── test_highlevel.py └── test_form.py ├── pyipptool ├── templates │ └── ipp │ │ ├── item.pt │ │ ├── constant_tuple.pt │ │ ├── form.pt │ │ └── group_tuple.pt ├── __init__.py ├── config.py ├── widgets.py ├── forms.py ├── schemas.py └── core.py ├── .gitignore ├── MANIFEST.in ├── tox.ini ├── LICENSE.txt ├── .travis.yml ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyipptool/templates/ipp/item.pt: -------------------------------------------------------------------------------- 1 | ${field.serialize(cstruct)} 2 | -------------------------------------------------------------------------------- /tests/hello.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezeep/pyipptool/HEAD/tests/hello.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg/ 4 | *.egg-info/ 5 | *.pyc 6 | .coverage 7 | .cache/ 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | include pyipptool/templates/ipp/* 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34 3 | [testenv] 4 | deps=pytest 5 | pytest-pep8 6 | future 7 | futures 8 | tornado 9 | pkipplib 10 | mock 11 | commands=py.test --pep8 pyipptool tests 12 | -------------------------------------------------------------------------------- /pyipptool/templates/ipp/constant_tuple.pt: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /pyipptool/templates/ipp/form.pt: -------------------------------------------------------------------------------- 1 | 2 | { 3 | 5 | } 6 | 7 | -------------------------------------------------------------------------------- /pyipptool/templates/ipp/group_tuple.pt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 ezeep GmbH 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | 6 | 7 | before_install: 8 | - sudo apt-get update 9 | - sudo apt-get install -qq cups cups-pdf 10 | - curl http://www.cups.org/software/ipptool/ipptool-20130731-linux-ubuntu-x86_64.tar.gz | tar xvzf - 11 | - "echo \"[main]\nipptool_path = \"$TRAVIS_BUILD_DIR\"/ipptool-20130731/ipptool\ncups_uri = http://localhost:631/\" > ~/.pyipptool.cfg" 12 | - cat ~/.pyipptool.cfg 13 | - echo "$USER:travis" | sudo chpasswd 14 | - sudo pip install -U pip setuptools 15 | 16 | install: 17 | - pip install -e . pytest-cov coveralls pytest mock tornado pkipplib pytest-pep8 18 | 19 | script: 20 | - py.test --cov pyipptool --ignore ipptool-20130731 --pep8 21 | 22 | after_success: 23 | coveralls 24 | 25 | notifications: 26 | email: false 27 | -------------------------------------------------------------------------------- /pyipptool/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import get_config 2 | from .core import IPPToolWrapper 3 | 4 | 5 | config = get_config() 6 | 7 | wrapper = IPPToolWrapper(config) 8 | create_job_subscription = wrapper.create_job_subscription 9 | create_printer_subscription = wrapper. create_printer_subscription 10 | cancel_job = wrapper.cancel_job 11 | release_job = wrapper.release_job 12 | create_job = wrapper.create_job 13 | create_printer_subscription = wrapper.create_printer_subscription 14 | cups_add_modify_class = wrapper.cups_add_modify_class 15 | cups_add_modify_printer = wrapper.cups_add_modify_printer 16 | cups_delete_printer = wrapper.cups_delete_printer 17 | cups_delete_class = wrapper.cups_delete_class 18 | cups_get_classes = wrapper.cups_get_classes 19 | cups_get_devices = wrapper.cups_get_devices 20 | cups_get_ppd = wrapper.cups_get_ppd 21 | cups_get_ppds = wrapper.cups_get_ppds 22 | cups_get_printers = wrapper.cups_get_printers 23 | cups_move_job = wrapper.cups_move_job 24 | cups_reject_jobs = wrapper.cups_reject_jobs 25 | get_job_attributes = wrapper.get_job_attributes 26 | get_jobs = wrapper.get_jobs 27 | get_printer_attributes = wrapper.get_printer_attributes 28 | get_subscriptions = wrapper.get_subscriptions 29 | get_notifications = wrapper.get_notifications 30 | pause_printer = wrapper.pause_printer 31 | print_job = wrapper.print_job 32 | resume_printer = wrapper.resume_printer 33 | send_document = wrapper.send_document 34 | hold_new_jobs = wrapper.hold_new_jobs 35 | release_held_new_jobs = wrapper.release_held_new_jobs 36 | cancel_subscription = wrapper.cancel_subscription 37 | -------------------------------------------------------------------------------- /pyipptool/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from future import standard_library 4 | with standard_library.hooks(): 5 | import configparser 6 | 7 | 8 | def read_config(paths=()): 9 | config = {} 10 | fs_config = configparser.ConfigParser() 11 | fs_config.read(paths) 12 | config['cups_uri'] = fs_config.get('main', 'cups_uri') 13 | config['ipptool_path'] = fs_config.get('main', 'ipptool_path') 14 | try: 15 | config['login'] = fs_config.get('main', 'login') 16 | except configparser.NoOptionError: 17 | pass 18 | try: 19 | config['password'] = fs_config.get('main', 'password') 20 | except configparser.NoOptionError: 21 | pass 22 | try: 23 | config['graceful_shutdown_time'] = fs_config.getint( 24 | 'main', 25 | 'graceful_shutdown_time') 26 | except configparser.NoOptionError: 27 | config['graceful_shutdown_time'] = 2 28 | try: 29 | config['timeout'] = fs_config.getint('main', 'timeout') 30 | except configparser.NoOptionError: 31 | config['timeout'] = 10 32 | return config 33 | 34 | 35 | class LazyConfig(dict): 36 | def __init__(self, paths): 37 | self.paths = paths 38 | self.loaded = False 39 | 40 | def __getitem__(self, key): 41 | if not self.loaded: 42 | self.update(read_config(self.paths)) 43 | self.loaded = True 44 | return super(LazyConfig, self).__getitem__(key) 45 | 46 | 47 | def get_config(paths=('/etc/opt/pyipptool/pyipptool.cfg', 48 | os.path.join(os.path.expanduser('~'), 49 | '.pyipptool.cfg'))): 50 | return LazyConfig(paths) 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup 4 | from setuptools.command.test import test as TestCommand 5 | 6 | 7 | class PyTest(TestCommand): 8 | def finalize_options(self): 9 | TestCommand.finalize_options(self) 10 | self.test_args = [] 11 | self.test_suite = True 12 | 13 | def run_tests(self): 14 | import pytest 15 | errno = pytest.main(self.test_args) 16 | sys.exit(errno) 17 | 18 | 19 | version = '0.5.0dev' 20 | 21 | 22 | def read_that_file(path): 23 | with open(path) as open_file: 24 | return open_file.read() 25 | 26 | 27 | description = '\n'.join((read_that_file('README.rst'), 28 | read_that_file('LICENSE.txt'))) 29 | 30 | setup( 31 | name='pyipptool', 32 | version=version, 33 | author='Nicolas Delaby', 34 | author_email='nicolas.delaby@ezeep.com', 35 | description='ipptool python wrapper', 36 | url='https://github.com/ezeep/pyipptool', 37 | long_description=description, 38 | license='Apache Software License', 39 | packages=('pyipptool',), 40 | install_requires=('deform>=2.0a2', 'future',), 41 | extra_requires={'Tornado': ('tornado', 'futures')}, 42 | tests_require=('mock', 'pytest', 'coverage', 'pytest-pep8', 43 | 'pytest-cov', 'coveralls', 'pkipplib', 'tornado', 44 | 'tox',), 45 | include_package_data=True, 46 | test_suite='tests', 47 | cmdclass = {'test': PyTest}, 48 | classifiers = [ 49 | 'Development Status :: 4 - Beta', 50 | 'Intended Audience :: Developers', 51 | 'Intended Audience :: System Administrators', 52 | 'License :: OSI Approved :: Apache Software License', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3.4', 56 | 'Topic :: Printing', 57 | ], 58 | keywords='cups ipptool ipp printing tornado', 59 | ) 60 | -------------------------------------------------------------------------------- /pyipptool/widgets.py: -------------------------------------------------------------------------------- 1 | import colander 2 | from deform.widget import MappingWidget, SequenceWidget, Widget 3 | from future.builtins import bytes, str 4 | 5 | 6 | class IPPDisplayWidget(Widget): 7 | def serialize(self, field, cstruct=None, readonly=False): 8 | return 'DISPLAY {}'.format(field.name.replace('_', '-')) 9 | 10 | 11 | class IPPNameWidget(Widget): 12 | def serialize(self, field, cstruct=None, readonly=False): 13 | name = field.name 14 | while field.parent is not None: 15 | field = field.parent 16 | value = getattr(field.schema, name) 17 | return '{} "{}"'.format(name.upper(), value) 18 | 19 | 20 | class IPPFileWidget(Widget): 21 | def serialize(self, field, cstruct=None, readonly=False): 22 | if cstruct is colander.null: 23 | return '' 24 | if not isinstance(cstruct, (str, bytes)): 25 | raise ValueError('Wrong value provided for field {!r}'.format( 26 | field.name)) 27 | return 'FILE {}'.format(cstruct) 28 | 29 | 30 | class IPPAttributeWidget(Widget): 31 | def serialize(self, field, cstruct=None, readonly=False): 32 | if cstruct is colander.null: 33 | return '' 34 | if cstruct is None: 35 | raise ValueError('None value provided for {!r}'.format(field.name)) 36 | attr_name = field.schema.typ.__class__.__name__ 37 | attr_name = attr_name[0].lower() + attr_name[1:] 38 | return 'ATTR {attr_type} {attr_name} {attr_value}'.format( 39 | attr_type=attr_name, 40 | attr_name=field.name.replace('_', '-'), 41 | attr_value=cstruct) 42 | 43 | 44 | class IPPBodyWidget(MappingWidget): 45 | readonly_template = 'ipp/form' 46 | template = readonly_template 47 | item_template = 'ipp/item' 48 | 49 | 50 | class IPPGroupWidget(SequenceWidget): 51 | readonly_template = 'ipp/group_tuple' 52 | template = readonly_template 53 | item_template = 'ipp/item' 54 | 55 | 56 | class IPPConstantTupleWidget(SequenceWidget): 57 | readonly_template = 'ipp/constant_tuple' 58 | template = readonly_template 59 | item_template = 'ipp/item' 60 | -------------------------------------------------------------------------------- /tests/test_with_cups.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | TRAVIS = os.getenv('TRAVIS') 6 | TRAVIS_USER = os.getenv('USER') 7 | TRAVIS_BUILD_DIR = os.getenv('TRAVIS_BUILD_DIR') 8 | 9 | 10 | @pytest.mark.skipif(TRAVIS != 'true', reason='requires travis') 11 | class TestWithCups(object): 12 | 13 | ipptool_path = '%s/ipptool-20130731/ipptool' % TRAVIS_BUILD_DIR 14 | config = {'ipptool_path': ipptool_path, 15 | 'cups_uri': 'http://localhost:631/', 16 | 'login': TRAVIS_USER, 17 | 'password': 'travis', 18 | 'graceful_shutdown_time': 2, 19 | 'timeout': 5} 20 | 21 | def test_cups_get_printers(self): 22 | import pyipptool 23 | ipptool = pyipptool.core.IPPToolWrapper(self.config) 24 | response = ipptool.cups_get_printers() 25 | assert response['Name'] == 'CUPS Get Printers' 26 | assert response['Operation'] == 'CUPS-Get-Printers' 27 | assert response['RequestAttributes'] == [{ 28 | 'attributes-charset': 'utf-8', 29 | 'attributes-natural-language': 'en'}] 30 | assert len(response['ResponseAttributes']) == 2 31 | assert response['ResponseAttributes'][1]['printer-name'] == 'PDF' 32 | assert response['StatusCode'] == 'successful-ok' 33 | assert response['Successful'] 34 | 35 | def test_cups_get_ppds(self): 36 | import pyipptool 37 | ipptool = pyipptool.core.IPPToolWrapper(self.config) 38 | response = ipptool.cups_get_ppds(ppd_make_and_model='generic pdf') 39 | assert response['Name'] == 'CUPS Get PPDs' 40 | assert response['Operation'] == 'CUPS-Get-PPDs' 41 | assert response['RequestAttributes'] == [{ 42 | 'attributes-charset': 'utf-8', 43 | 'attributes-natural-language': 'en', 44 | 'ppd-make-and-model': 'generic pdf'}] 45 | assert len(response['ResponseAttributes']) == 2 46 | assert 'ppd-make-and-model' in response['ResponseAttributes'][1] 47 | ppd = response['ResponseAttributes'][1] 48 | assert ppd['ppd-make-and-model'] == 'Generic PDF Printer' 49 | assert response['StatusCode'] == 'successful-ok' 50 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyipptool 2 | ========= 3 | 4 | .. image:: 5 | https://travis-ci.org/ezeep/pyipptool.svg?branch=master 6 | :target: https://travis-ci.org/ezeep/pyipptool 7 | 8 | .. image:: https://coveralls.io/repos/ezeep/pyipptool/badge.png 9 | :target: https://coveralls.io/r/ezeep/pyipptool 10 | 11 | .. image:: https://pypip.in/v/pyipptool/badge.png 12 | :target: https://crate.io/packages/pyipptool/ 13 | :alt: Latest PyPI version 14 | 15 | .. image:: https://landscape.io/github/ezeep/pyipptool/master/landscape.png 16 | :target: https://landscape.io/github/ezeep/pyipptool/master 17 | :alt: Code Health 18 | 19 | 20 | Convenient IPP request generator for python to interrogate CUPS or IPP devices, with the help of ipptool_. 21 | 22 | .. _ipptool: http://www.cups.org/documentation.php/doc-1.7/man-ipptool.html 23 | 24 | Setup 25 | ----- 26 | 27 | .. code-block:: console 28 | 29 | python setup.py install 30 | 31 | 32 | Tests 33 | ----- 34 | 35 | .. code-block:: console 36 | 37 | python setup.py test 38 | 39 | Configuration 40 | ------------- 41 | 42 | Add the following content in ``~/.pyipptool.cfg`` or ``/etc/pyipptool/pyipptol.cfg``. 43 | 44 | .. code-block:: ini 45 | 46 | [main] 47 | ipptool_path = /usr/bin/ipptool 48 | cups_uri = http://localhost:631/ 49 | ;If authentication is required 50 | login = admin 51 | password = secret 52 | graceful_shutdown_time = 2 53 | timeout = 10 54 | 55 | 56 | Where ``ipptool_path`` points to the absolute path of your installed ipptool 57 | 58 | Usage 59 | ----- 60 | 61 | Create an infinite time subscription for printer-XYZ class for the ``rss`` notifier 62 | 63 | .. code-block:: python 64 | 65 | >>> from pyipptool import create_printer_subscription 66 | >>> create_printer_subscription( 67 | printer_uri='http://localhost:631/classes/printer-XYZ', 68 | requesting_user_name='admin', 69 | notify_recipient_uri='rss://', 70 | notify_events='all', 71 | notify_lease_duration=0) 72 | {'Name': 'Create Printer Subscription', 73 | 'Operation': 'Create-Printer-Subscription', 74 | 'RequestAttributes': [{'attributes-charset': 'utf-8', 75 | 'attributes-natural-language': 'en', 76 | 'printer-uri': 'http://localhost:631/classes/printer-XYZ', 77 | 'requesting-user-name': 'admin'}, 78 | {'notify-events': 'all', 79 | 'notify-lease-duration': 0, 80 | 'notify-recipient-uri': 'rss://'}], 81 | 'ResponseAttributes': [{'attributes-charset': 'utf-8', 82 | 'attributes-natural-language': 'en'}, 83 | {'notify-subscription-id': 23}], 84 | 'StatusCode': 'successful-ok', 85 | 'Successful': True, 86 | 'notify-subscription-id': 23} 87 | -------------------------------------------------------------------------------- /tests/test_widget.py: -------------------------------------------------------------------------------- 1 | import colander 2 | from deform.tests.test_widget import DummyField, DummyRenderer 3 | import pytest 4 | 5 | 6 | class DummyType(object): 7 | pass 8 | 9 | 10 | class DummySchema(object): 11 | def __init__(self, **kw): 12 | self.__dict__.update(kw) 13 | 14 | 15 | def test_ipp_attribute_widget_null(): 16 | from pyipptool.widgets import IPPAttributeWidget 17 | widget = IPPAttributeWidget() 18 | rendrer = DummyRenderer() 19 | field = DummyField(None, renderer=rendrer) 20 | response = widget.serialize(field, colander.null) 21 | assert response == '' 22 | 23 | 24 | def test_ipp_attribute_widget_with_value(): 25 | from pyipptool.widgets import IPPAttributeWidget 26 | widget = IPPAttributeWidget() 27 | rendrer = DummyRenderer() 28 | schema = DummySchema() 29 | schema.typ = DummyType() 30 | field = DummyField(schema, renderer=rendrer) 31 | response = widget.serialize(field, 'hello') 32 | assert response == 'ATTR dummyType name hello' 33 | 34 | 35 | def test_ipp_attribute_widget_with_None(): 36 | from pyipptool.widgets import IPPAttributeWidget 37 | widget = IPPAttributeWidget() 38 | rendrer = DummyRenderer() 39 | schema = DummySchema() 40 | schema.typ = DummyType() 41 | field = DummyField(schema, renderer=rendrer) 42 | with pytest.raises(ValueError) as exc_info: 43 | widget.serialize(field, None) 44 | assert str(exc_info.value) == "None value provided for 'name'" 45 | 46 | 47 | def test_ipp_display_widget(): 48 | from pyipptool.widgets import IPPDisplayWidget 49 | widget = IPPDisplayWidget() 50 | rendrer = DummyRenderer() 51 | field = DummyField(None, renderer=rendrer) 52 | response = widget.serialize(field, None) 53 | assert response == 'DISPLAY name' 54 | 55 | 56 | def test_ipp_name_widget(): 57 | from pyipptool.widgets import IPPNameWidget 58 | widget = IPPNameWidget() 59 | rendrer = DummyRenderer() 60 | schema = DummySchema(name='FOO') 61 | 62 | field = DummyField(schema, renderer=rendrer) 63 | field.parent = None 64 | sub_field = DummyField(None, renderer=rendrer) 65 | sub_field.parent = field 66 | 67 | response = widget.serialize(sub_field, 'world') 68 | assert response == 'NAME "FOO"' 69 | 70 | 71 | def test_ipp_file_widget(): 72 | from pyipptool.widgets import IPPFileWidget 73 | 74 | widget = IPPFileWidget() 75 | rendrer = DummyRenderer() 76 | 77 | field = DummyField(None, renderer=rendrer) 78 | response = widget.serialize(field, '/path/to/file.txt') 79 | assert response == 'FILE /path/to/file.txt' 80 | 81 | 82 | def test_ipp_file_widget_with_None(): 83 | from pyipptool.widgets import IPPFileWidget 84 | 85 | widget = IPPFileWidget() 86 | rendrer = DummyRenderer() 87 | 88 | field = DummyField(None, renderer=rendrer) 89 | with pytest.raises(ValueError): 90 | widget.serialize(field, None) 91 | -------------------------------------------------------------------------------- /pyipptool/forms.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import resource_filename 2 | 3 | import deform.template 4 | 5 | from .schemas import (cancel_job_schema, 6 | release_job_schema, 7 | create_job_schema, 8 | create_job_subscription_schema, 9 | create_printer_subscription_schema, 10 | cups_add_modify_class_schema, 11 | cups_add_modify_printer_schema, 12 | cups_delete_printer_schema, 13 | cups_delete_class_schema, 14 | cups_get_classes_schema, 15 | cups_get_devices_schema, 16 | cups_get_ppd_schema, 17 | cups_get_ppds_schema, 18 | cups_get_printers_schema, 19 | cups_move_job_schema, 20 | cups_reject_jobs_schema, 21 | get_job_attributes_schema, 22 | get_jobs_schema, 23 | get_printer_attributes_schema, 24 | get_subscriptions_schema, 25 | get_notifications_schema, 26 | pause_printer_schema, 27 | print_job_schema, 28 | resume_printer_schema, 29 | send_document_schema, 30 | hold_new_jobs_schema, 31 | release_held_new_jobs_schema, 32 | cancel_subscription_schema, 33 | ) 34 | 35 | default_dir = resource_filename('pyipptool', 'templates/') 36 | renderer = deform.template.ZPTRendererFactory((default_dir,)) 37 | 38 | deform.Form.set_default_renderer(renderer) 39 | 40 | 41 | cancel_job_form = deform.Form(cancel_job_schema) 42 | release_job_form = deform.Form(release_job_schema) 43 | create_job_form = deform.Form(create_job_schema) 44 | create_job_subscription_form = deform.Form( 45 | create_job_subscription_schema) 46 | create_printer_subscription_form = deform.Form( 47 | create_printer_subscription_schema) 48 | cups_add_modify_printer_form = deform.Form(cups_add_modify_printer_schema) 49 | cups_delete_printer_form = deform.Form(cups_delete_printer_schema) 50 | cups_delete_class_form = deform.Form(cups_delete_class_schema) 51 | cups_add_modify_class_form = deform.Form(cups_add_modify_class_schema) 52 | cups_get_classes_form = deform.Form(cups_get_classes_schema) 53 | cups_get_devices_form = deform.Form(cups_get_devices_schema) 54 | cups_get_ppd_form = deform.Form(cups_get_ppd_schema) 55 | cups_get_ppds_form = deform.Form(cups_get_ppds_schema) 56 | cups_get_printers_form = deform.Form(cups_get_printers_schema) 57 | cups_move_job_form = deform.Form(cups_move_job_schema) 58 | cups_reject_jobs_form = deform.Form(cups_reject_jobs_schema) 59 | get_job_attributes_form = deform.Form(get_job_attributes_schema) 60 | get_jobs_form = deform.Form(get_jobs_schema) 61 | get_printer_attributes_form = deform.Form(get_printer_attributes_schema) 62 | get_subscriptions_form = deform.Form(get_subscriptions_schema) 63 | get_notifications_form = deform.Form(get_notifications_schema) 64 | pause_printer_form = deform.Form(pause_printer_schema) 65 | print_job_form = deform.Form(print_job_schema) 66 | resume_printer_form = deform.Form(resume_printer_schema) 67 | send_document_form = deform.Form(send_document_schema) 68 | hold_new_jobs_form = deform.Form(hold_new_jobs_schema) 69 | release_held_new_jobs_form = deform.Form(release_held_new_jobs_schema) 70 | cancel_subscription_form = deform.Form(cancel_subscription_schema) 71 | -------------------------------------------------------------------------------- /tests/test_async_subprocess.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import threading 4 | import time 5 | import sys 6 | 7 | from future import standard_library 8 | from future.utils import PY3 9 | with standard_library.hooks(): 10 | import http.server 11 | import socketserver 12 | 13 | import pytest 14 | import tornado.testing 15 | from past import autotranslate 16 | autotranslate(['pkipplib']) 17 | from pkipplib import pkipplib 18 | 19 | 20 | TRAVIS_USER = os.getenv('TRAVIS_USER', 'travis') 21 | TRAVIS_BUILD_DIR = os.getenv('TRAVIS_BUILD_DIR') 22 | 23 | 24 | @pytest.mark.skipif(sys.version_info > (3,), 25 | reason='pkipplib is only python2 compatible') 26 | class AsyncSubprocessTestCase(tornado.testing.AsyncTestCase): 27 | 28 | ipptool_path = ('%s/ipptool-20130731/ipptool' % TRAVIS_BUILD_DIR if 29 | TRAVIS_BUILD_DIR else '/usr/bin/ipptool') 30 | config = {'ipptool_path': ipptool_path, 31 | 'login': TRAVIS_USER, 32 | 'password': 'travis', 33 | 'graceful_shutdown_time': 2, 34 | 'timeout': 5} 35 | 36 | @tornado.testing.gen_test 37 | def test_async_call(self): 38 | from pyipptool.core import AsyncIPPToolWrapper 39 | from pyipptool.forms import get_subscriptions_form 40 | 41 | class Handler(http.server.BaseHTTPRequestHandler): 42 | """ 43 | HTTP Handler that will make ipptool waiting 44 | """ 45 | protocol_version = 'HTTP/1.1' 46 | 47 | def do_POST(self): 48 | # return a real IPP Response thanks to pkipplib 49 | if PY3: 50 | content_length = int(self.headers.get('content-length')) 51 | else: 52 | content_length = int( 53 | self.headers.getheader('content-length')) 54 | ipp_request = pkipplib.IPPRequest( 55 | self.rfile.read(content_length)) 56 | ipp_request.parse() 57 | try: 58 | self.send_response(200) 59 | self.send_header('Content-type', 'application/ipp') 60 | self.end_headers() 61 | ipp_response = pkipplib.IPPRequest( 62 | operation_id=pkipplib.IPP_OK, 63 | request_id=ipp_request.request_id) 64 | ipp_response.operation['attributes-charset'] =\ 65 | ('charset', 'utf-8') 66 | ipp_response.operation['attributes-natural-language'] =\ 67 | ('naturalLanguage', 'en-us') 68 | self.wfile.write(ipp_response.dump()) 69 | finally: 70 | assassin = threading.Thread(target=self.server.shutdown) 71 | assassin.daemon = True 72 | assassin.start() 73 | 74 | PORT = 6789 75 | while True: 76 | try: 77 | httpd = socketserver.TCPServer(("", PORT), Handler) 78 | except socket.error as exe: 79 | if exe.errno in (48, 98): 80 | PORT += 1 81 | else: 82 | raise 83 | else: 84 | break 85 | httpd.allow_reuse_address = True 86 | 87 | thread = threading.Thread(target=httpd.serve_forever) 88 | thread.daemon = True 89 | thread.start() 90 | 91 | wrapper = AsyncIPPToolWrapper(self.config, self.io_loop) 92 | wrapper.config['cups_uri'] = 'http://localhost:%s/' % PORT 93 | 94 | request = get_subscriptions_form.render( 95 | {'operation_attributes_tag': 96 | {'printer_uri': 'http://localhost:%s/printers/fake' % PORT}} 97 | ) 98 | 99 | response = yield wrapper._call_ipptool(request) 100 | for key in ('RequestId', 'ipptoolVersion', 'Version'): 101 | # May diverge depending of version of ipptool 102 | try: 103 | del response[key] 104 | except KeyError: 105 | pass 106 | expected_response = {'Name': 'Get Subscriptions', 107 | 'Operation': 'Get-Subscriptions', 108 | 'Successful': True, 109 | 'RequestAttributes': 110 | [{'attributes-charset': 'utf-8', 111 | 'attributes-natural-language': 'en', 112 | 'printer-uri': 113 | 'http://localhost:%s/printers/fake' % PORT}], 114 | 'ResponseAttributes': 115 | [{'attributes-charset': 'utf-8', 116 | 'attributes-natural-language': 'en-us'}], 117 | 'StatusCode': 'successful-ok', 118 | 'Successful': True} 119 | assert response == expected_response, response 120 | 121 | @tornado.testing.gen_test 122 | def test_async_timeout_call(self): 123 | from pyipptool.core import AsyncIPPToolWrapper, TimeoutError 124 | from pyipptool.forms import get_subscriptions_form 125 | 126 | class Handler(http.server.BaseHTTPRequestHandler): 127 | """ 128 | HTTP Handler that will make ipptool waiting 129 | """ 130 | protocol_version = 'HTTP/1.1' 131 | 132 | def do_POST(self): 133 | time.sleep(0.2) 134 | assassin = threading.Thread(target=self.server.shutdown) 135 | assassin.daemon = True 136 | assassin.start() 137 | 138 | PORT = 6789 139 | while True: 140 | try: 141 | httpd = socketserver.TCPServer(("", PORT), Handler) 142 | except socket.error as exe: 143 | if exe.errno in (48, 98): 144 | PORT += 1 145 | else: 146 | raise 147 | else: 148 | break 149 | httpd.allow_reuse_address = True 150 | 151 | thread = threading.Thread(target=httpd.serve_forever) 152 | thread.daemon = True 153 | thread.start() 154 | 155 | wrapper = AsyncIPPToolWrapper(self.config, self.io_loop) 156 | wrapper.config['cups_uri'] = 'http://localhost:%s/' % PORT 157 | request = get_subscriptions_form.render( 158 | {'header': 159 | {'operation_attributes': 160 | {'printer_uri': 'http://localhost:%s/printers/fake' % PORT}}} 161 | ) 162 | 163 | try: 164 | old_timeout = wrapper.config['timeout'] 165 | wrapper.config['timeout'] = .1 166 | with pytest.raises(TimeoutError): 167 | yield wrapper._call_ipptool(request) 168 | finally: 169 | wrapper.config['timeout'] = old_timeout 170 | -------------------------------------------------------------------------------- /pyipptool/schemas.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import colander 4 | from .widgets import (IPPAttributeWidget, 5 | IPPBodyWidget, 6 | IPPFileWidget, 7 | IPPGroupWidget, 8 | IPPNameWidget) 9 | 10 | 11 | class IntegerOrTuple(colander.List): 12 | def serialize(self, node, appstruct): 13 | if isinstance(appstruct, (tuple, list)): 14 | return super(IntegerOrTuple, self).serialize( 15 | node, 16 | ','.join([str(i) for i in appstruct])) 17 | return super(IntegerOrTuple, self).serialize(node, appstruct) 18 | 19 | 20 | class StringOrTuple(colander.String): 21 | def serialize(self, node, appstruct): 22 | if isinstance(appstruct, (tuple, list)): 23 | return super(StringOrTuple, self).serialize(node, 24 | ','.join(appstruct)) 25 | return super(StringOrTuple, self).serialize(node, appstruct) 26 | 27 | 28 | class Charset(colander.String): 29 | pass 30 | 31 | 32 | class Enum(colander.String): 33 | pass 34 | 35 | 36 | class Integer(IntegerOrTuple): 37 | pass 38 | 39 | 40 | class Keyword(StringOrTuple): 41 | pass 42 | 43 | 44 | class Language(colander.String): 45 | pass 46 | 47 | 48 | class MimeMediaType(colander.String): 49 | pass 50 | 51 | 52 | class Name(colander.String): 53 | pass 54 | 55 | 56 | class NaturalLanguage(colander.String): 57 | pass 58 | 59 | 60 | class RangeOfInteger(colander.String): 61 | pass 62 | 63 | 64 | class Resolution(colander.String): 65 | pass 66 | 67 | 68 | range_regex = re.compile('^(\d+)-(\d+)$') 69 | 70 | 71 | def range_of_integer_validator(node, value): 72 | match = range_regex.match(value) 73 | if match is None: 74 | raise colander.Invalid(node, 75 | "Invalid range e.g '1-5'") 76 | low, high = match.groups() 77 | if int(low) >= int(high): 78 | raise colander.Invalid(node, 79 | 'Low bound must be lower than high bound') 80 | 81 | 82 | class Text(colander.String): 83 | def serialize(self, node, appstruct): 84 | if appstruct is None: 85 | raise ValueError('None value provided for {!r}'.format(node.name)) 86 | if appstruct is colander.null: 87 | return colander.null 88 | value = super(Text, self).serialize(node, appstruct) 89 | return '"{}"'.format(value) 90 | 91 | 92 | class Uri(StringOrTuple): 93 | pass 94 | 95 | 96 | class OperationAttributesGroup(colander.Schema): 97 | attributes_charset = colander.SchemaNode(Charset(), 98 | default='utf-8', 99 | widget=IPPAttributeWidget()) 100 | attributes_natural_language = colander.SchemaNode( 101 | Language(), 102 | default='en', 103 | widget=IPPAttributeWidget()) 104 | compression = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 105 | printer_uri = colander.SchemaNode(Uri(), 106 | widget=IPPAttributeWidget()) 107 | job_uri = colander.SchemaNode(Uri(), widget=IPPAttributeWidget()) 108 | job_id = colander.SchemaNode(colander.Integer(), 109 | widget=IPPAttributeWidget()) 110 | exclude_schemes = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 111 | include_schemes = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 112 | limit = colander.SchemaNode( 113 | colander.Integer(), 114 | widget=IPPAttributeWidget()) 115 | requested_attributes = colander.SchemaNode( 116 | Keyword(), 117 | widget=IPPAttributeWidget()) 118 | purge_job = colander.SchemaNode(colander.Boolean(true_val=1, false_val=0), 119 | widget=IPPAttributeWidget()) 120 | requesting_user_name = colander.SchemaNode(Name(), 121 | widget=IPPAttributeWidget()) 122 | job_name = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 123 | ipp_attribute_fidelity = colander.SchemaNode(colander.Boolean(true_val=1, 124 | false_val=0), 125 | widget=IPPAttributeWidget()) 126 | job_k_octets = colander.SchemaNode(Integer(), widget=IPPAttributeWidget()) 127 | job_impressions = colander.SchemaNode(Integer(), 128 | widget=IPPAttributeWidget()) 129 | job_media_sheets = colander.SchemaNode(Integer(), 130 | widget=IPPAttributeWidget()) 131 | document_name = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 132 | device_class = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 133 | document_format = colander.SchemaNode(MimeMediaType(), 134 | widget=IPPAttributeWidget()) 135 | document_natural_language = colander.SchemaNode( 136 | NaturalLanguage(), 137 | widget=IPPAttributeWidget()) 138 | 139 | notify_subscription_id = colander.SchemaNode( 140 | colander.Integer(), 141 | widget=IPPAttributeWidget()) 142 | 143 | printer_message_from_operator = colander.SchemaNode( 144 | Text(), widget=IPPAttributeWidget()) 145 | 146 | limit = colander.SchemaNode( 147 | colander.Integer(), 148 | widget=IPPAttributeWidget()) 149 | requested_attributes = colander.SchemaNode( 150 | Keyword(), 151 | widget=IPPAttributeWidget()) 152 | which_jobs = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 153 | my_jobs = colander.SchemaNode(colander.Boolean(true_val=1, false_val=0), 154 | widget=IPPAttributeWidget()) 155 | notify_job_id = colander.SchemaNode( 156 | colander.Integer(), 157 | widget=IPPAttributeWidget()) 158 | limit = colander.SchemaNode( 159 | colander.Integer(), 160 | widget=IPPAttributeWidget()) 161 | requested_attributes = colander.SchemaNode( 162 | Keyword(), 163 | widget=IPPAttributeWidget()) 164 | my_subscriptions = colander.SchemaNode( 165 | colander.Boolean(false_val=0, true_val=1), 166 | widget=IPPAttributeWidget()) 167 | 168 | notify_subscription_ids = colander.SchemaNode( 169 | Integer(), 170 | widget=IPPAttributeWidget()) 171 | notify_sequence_numbers = colander.SchemaNode( 172 | Integer(), 173 | widget=IPPAttributeWidget()) 174 | notify_wait = colander.SchemaNode( 175 | colander.Boolean(false_val=0, true_val=1), 176 | widget=IPPAttributeWidget()) 177 | 178 | ppd_make = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 179 | ppd_make_and_model = colander.SchemaNode( 180 | Text(), 181 | widget=IPPAttributeWidget()) 182 | ppd_model_number = colander.SchemaNode(colander.Integer(), 183 | widget=IPPAttributeWidget()) 184 | ppd_name = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 185 | ppd_natural_language = colander.SchemaNode(NaturalLanguage(), 186 | widget=IPPAttributeWidget()) 187 | ppd_product = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 188 | ppd_psversion = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 189 | ppd_type = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 190 | 191 | timeout = colander.SchemaNode( 192 | colander.Integer(), 193 | widget=IPPAttributeWidget()) 194 | 195 | first_printer_name = colander.SchemaNode( 196 | Name(), 197 | widget=IPPAttributeWidget()) 198 | printer_location = colander.SchemaNode( 199 | Text(), 200 | widget=IPPAttributeWidget()) 201 | printer_type = colander.SchemaNode(Enum(), widget=IPPAttributeWidget()) 202 | printer_type_mask = colander.SchemaNode( 203 | Enum(), 204 | widget=IPPAttributeWidget()) 205 | requested_attributes = colander.SchemaNode( 206 | Keyword(), 207 | widget=IPPAttributeWidget()) 208 | requested_user_name = colander.SchemaNode(Name(), 209 | widget=IPPAttributeWidget()) 210 | 211 | document_name = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 212 | compression = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 213 | document_format = colander.SchemaNode(MimeMediaType(), 214 | widget=IPPAttributeWidget()) 215 | document_natural_language = colander.SchemaNode( 216 | NaturalLanguage(), 217 | widget=IPPAttributeWidget()) 218 | last_document = colander.SchemaNode( 219 | colander.Boolean(false_val=0, true_val=1), 220 | widget=IPPAttributeWidget()) 221 | 222 | 223 | class JobAttributesGroup(colander.Schema): 224 | job_priority = colander.SchemaNode(Integer(), 225 | widget=IPPAttributeWidget()) 226 | job_printer_uri = colander.SchemaNode(Uri(), 227 | widget=IPPAttributeWidget()) 228 | job_hold_until = colander.SchemaNode(Keyword(), 229 | widget=IPPAttributeWidget()) 230 | job_sheets = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 231 | auth_info = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 232 | job_billing = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 233 | multiple_document_handling = colander.SchemaNode( 234 | Keyword(), 235 | widget=IPPAttributeWidget()) 236 | copies = colander.SchemaNode(Integer(), widget=IPPAttributeWidget()) 237 | finishings = colander.SchemaNode(Enum(), widget=IPPAttributeWidget()) 238 | page_ranges = colander.SchemaNode(RangeOfInteger(), 239 | widget=IPPAttributeWidget(), 240 | validator=range_of_integer_validator) 241 | sides = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 242 | number_up = colander.SchemaNode(Integer(), widget=IPPAttributeWidget()) 243 | orientation_requested = colander.SchemaNode(Enum(), 244 | widget=IPPAttributeWidget()) 245 | media = colander.SchemaNode(Keyword(), widget=IPPAttributeWidget()) 246 | printer_resolution = colander.SchemaNode(Resolution(), 247 | widget=IPPAttributeWidget()) 248 | print_quality = colander.SchemaNode(Enum(), widget=IPPAttributeWidget()) 249 | 250 | # Arbitrary Job Attributes 251 | ezeep_job_uuid = colander.SchemaNode(Text(), 252 | widget=IPPAttributeWidget()) 253 | 254 | 255 | class SubscriptionGroup(colander.Schema): 256 | notify_recipient_uri = colander.SchemaNode(Uri(), 257 | widget=IPPAttributeWidget()) 258 | notify_events = colander.SchemaNode(Keyword(), 259 | widget=IPPAttributeWidget()) 260 | notify_job_id = colander.SchemaNode(colander.Integer(), 261 | widget=IPPAttributeWidget()) 262 | notify_pull_method = colander.SchemaNode(Keyword(), 263 | widget=IPPAttributeWidget()) 264 | notify_attributes = colander.SchemaNode(Keyword(), 265 | widget=IPPAttributeWidget()) 266 | notify_charset = colander.SchemaNode(Charset(), 267 | widget=IPPAttributeWidget()) 268 | notify_natural_language = colander.SchemaNode(Language(), 269 | widget=IPPAttributeWidget()) 270 | notify_time_interval = colander.SchemaNode(colander.Integer(), 271 | widget=IPPAttributeWidget()) 272 | notify_lease_duration = colander.SchemaNode(colander.Integer(), 273 | widget=IPPAttributeWidget()) 274 | 275 | 276 | class DocumentAttributesGroup(colander.Schema): 277 | file = colander.SchemaNode(colander.String(), widget=IPPFileWidget()) 278 | 279 | 280 | class PrinterAttributesGroup(colander.Schema): 281 | auth_info_required = colander.SchemaNode(Keyword(), 282 | widget=IPPAttributeWidget()) 283 | 284 | printer_is_accepting_jobs = colander.SchemaNode( 285 | colander.Boolean(false_val=0, true_val=1), 286 | widget=IPPAttributeWidget()) 287 | printer_info = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 288 | printer_location = colander.SchemaNode(Text(), widget=IPPAttributeWidget()) 289 | printer_more_info = colander.SchemaNode(Uri(), widget=IPPAttributeWidget()) 290 | printer_op_policy = colander.SchemaNode(Name(), 291 | widget=IPPAttributeWidget()) 292 | printer_state = colander.SchemaNode(Enum(), widget=IPPAttributeWidget()) 293 | printer_state_message = colander.SchemaNode( 294 | Text(), 295 | widget=IPPAttributeWidget()) 296 | requesting_user_name_allowed = colander.SchemaNode( 297 | Name(), 298 | widget=IPPAttributeWidget()) 299 | requesting_user_name_denied = colander.SchemaNode( 300 | Name(), 301 | widget=IPPAttributeWidget()) 302 | printer_is_shared = colander.SchemaNode(colander.Boolean(true_val=1, 303 | false_val=0), 304 | widget=IPPAttributeWidget()) 305 | job_sheets_default = colander.SchemaNode(Name(), 306 | widget=IPPAttributeWidget()) 307 | device_uri = colander.SchemaNode(Uri(), 308 | widget=IPPAttributeWidget()) 309 | port_monitor = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 310 | ppd_name = colander.SchemaNode(Name(), widget=IPPAttributeWidget()) 311 | member_uris = colander.SchemaNode(Uri(), widget=IPPAttributeWidget()) 312 | 313 | # Always last 314 | file = colander.SchemaNode(colander.String(), widget=IPPFileWidget()) 315 | 316 | 317 | class BaseIPPSchema(colander.Schema): 318 | name = colander.SchemaNode(colander.String(), widget=IPPNameWidget()) 319 | operation = colander.SchemaNode(colander.String(), widget=IPPNameWidget()) 320 | operation_attributes_tag = OperationAttributesGroup( 321 | widget=IPPGroupWidget()) 322 | 323 | 324 | class CancelJobSchema(BaseIPPSchema): 325 | name = 'Cancel Job' 326 | operation = 'Cancel-Job' 327 | 328 | 329 | class ReleaseJobSchema(BaseIPPSchema): 330 | name = 'Release Job' 331 | operation = 'Release-Job' 332 | 333 | 334 | class CupsAddModifyPrinterSchema(BaseIPPSchema): 335 | name = 'CUPS Add Modify Printer' 336 | operation = 'CUPS-Add-Modify-Printer' 337 | printer_attributes_tag = PrinterAttributesGroup(widget=IPPGroupWidget()) 338 | 339 | 340 | class CupsDeletePrinterSchema(BaseIPPSchema): 341 | name = 'CUPS Delete Printer' 342 | operation = 'CUPS-Delete-Printer' 343 | 344 | 345 | class CupsAddModifyClassSchema(BaseIPPSchema): 346 | name = 'CUPS Add Modify Class' 347 | operation = 'CUPS-Add-Modify-Class' 348 | printer_attributes_tag = PrinterAttributesGroup(widget=IPPGroupWidget()) 349 | 350 | 351 | class CupsDeleteClassSchema(CupsDeletePrinterSchema): 352 | name = 'CUPS Delete Class' 353 | operation = 'CUPS-Delete-Class' 354 | 355 | 356 | class CupsGetClassesSchema(BaseIPPSchema): 357 | name = 'CUPS Get Classes' 358 | operation = 'CUPS-Get-Classes' 359 | 360 | 361 | class CupsGetDevicesSchema(BaseIPPSchema): 362 | name = 'CUPS Get Devices' 363 | operation = 'CUPS-Get-Devices' 364 | 365 | 366 | class CupsGetPPDSchema(BaseIPPSchema): 367 | name = 'CUPS Get PPD' 368 | operation = 'CUPS-Get-PPD' 369 | 370 | 371 | class CupsGetPPDsSchema(BaseIPPSchema): 372 | name = 'CUPS Get PPDs' 373 | operation = 'CUPS-Get-PPDs' 374 | 375 | 376 | class CupsGetPrintersSchema(CupsGetClassesSchema): 377 | name = 'CUPS Get Printers' 378 | operation = 'CUPS-Get-Printers' 379 | 380 | 381 | class CupsMoveJobSchema(BaseIPPSchema): 382 | name = 'CUPS Move Job' 383 | operation = 'CUPS-Move-Job' 384 | job_attributes_tag = JobAttributesGroup(widget=IPPGroupWidget()) 385 | 386 | 387 | class CupsRejectJobsSchema(BaseIPPSchema): 388 | name = 'CUPS Reject Jobs' 389 | operation = 'CUPS-Reject-Jobs' 390 | printer_attributes_tag = PrinterAttributesGroup(widget=IPPGroupWidget()) 391 | 392 | 393 | class CreateJobSchema(BaseIPPSchema): 394 | """ 395 | http://www.cups.org/documentation.php/spec-ipp.html#CREATE_JOB 396 | """ 397 | name = 'Create Job' 398 | operation = 'Create-Job' 399 | job_attributes_tag = JobAttributesGroup(widget=IPPGroupWidget()) 400 | 401 | 402 | class CreateJobSubscriptionSchema(BaseIPPSchema): 403 | name = 'Create Job Subscription' 404 | operation = 'Create-Job-Subscription' 405 | subscription_attributes_tag = SubscriptionGroup( 406 | widget=IPPGroupWidget()) 407 | 408 | 409 | class CreatePrinterSubscriptionSchema(CreateJobSubscriptionSchema): 410 | name = 'Create Printer Subscription' 411 | operation = 'Create-Printer-Subscription' 412 | 413 | 414 | class GetJobAttributesSchema(BaseIPPSchema): 415 | name = 'Get Job Attributes' 416 | operation = 'Get-Job-Attributes' 417 | 418 | 419 | class GetJobsSchema(BaseIPPSchema): 420 | name = 'Get Jobs' 421 | operation = 'Get-Jobs' 422 | 423 | 424 | class GetPrinterAttributesSchema(BaseIPPSchema): 425 | name = 'Get Printer Attributes' 426 | operation = 'Get-Printer-Attributes' 427 | 428 | 429 | class GetSubscriptionsSchema(BaseIPPSchema): 430 | name = 'Get Subscriptions' 431 | operation = 'Get-Subscriptions' 432 | 433 | 434 | class GetNotificationsSchema(BaseIPPSchema): 435 | name = 'Get Notifications' 436 | operation = 'Get-Notifications' 437 | 438 | 439 | class PausePrinterSchema(BaseIPPSchema): 440 | name = 'Pause Printer' 441 | operation = 'Pause-Printer' 442 | 443 | 444 | class PrintJobSchema(CreateJobSchema): 445 | name = 'Print Job' 446 | operation = 'Print-Job' 447 | job_attributes_tag = JobAttributesGroup(widget=IPPGroupWidget()) 448 | subscription_attributes_tag = SubscriptionGroup( 449 | widget=IPPGroupWidget()) 450 | document_attributes_tag = DocumentAttributesGroup(widget=IPPGroupWidget()) 451 | 452 | 453 | class ResumePrinterSchema(PausePrinterSchema): 454 | name = 'Resume Printer' 455 | operation = 'Resume-Printer' 456 | 457 | 458 | class SendDocumentSchema(BaseIPPSchema): 459 | name = 'Send Document' 460 | operation = 'Send-Document' 461 | document_attributes_tag = DocumentAttributesGroup(widget=IPPGroupWidget()) 462 | 463 | 464 | class HoldNewJobsSchema(PausePrinterSchema): 465 | name = 'Hold New Jobs' 466 | operation = 'Hold-New-Jobs' 467 | 468 | 469 | class ReleaseHeldNewJobsSchema(HoldNewJobsSchema): 470 | name = 'Release Held New Jobs' 471 | operation = 'Release-Held-New-Jobs' 472 | 473 | 474 | class CancelSubscriptionSchema(BaseIPPSchema): 475 | name = 'Cancel Subscription' 476 | operation = 'Cancel-Subscription' 477 | 478 | 479 | cancel_job_schema = CancelJobSchema(widget=IPPBodyWidget()) 480 | 481 | release_job_schema = ReleaseJobSchema(widget=IPPBodyWidget()) 482 | 483 | create_job_subscription_schema = CreateJobSubscriptionSchema( 484 | widget=IPPBodyWidget()) 485 | 486 | create_job_schema = CreateJobSchema(widget=IPPBodyWidget()) 487 | 488 | create_printer_subscription_schema = CreatePrinterSubscriptionSchema( 489 | widget=IPPBodyWidget()) 490 | 491 | cups_add_modify_class_schema = CupsAddModifyClassSchema( 492 | widget=IPPBodyWidget()) 493 | 494 | cups_add_modify_printer_schema = CupsAddModifyPrinterSchema( 495 | widget=IPPBodyWidget()) 496 | 497 | cups_delete_printer_schema = CupsDeletePrinterSchema( 498 | widget=IPPBodyWidget()) 499 | 500 | cups_delete_class_schema = CupsDeleteClassSchema( 501 | widget=IPPBodyWidget()) 502 | 503 | cups_get_classes_schema = CupsGetClassesSchema(widget=IPPBodyWidget()) 504 | 505 | cups_get_devices_schema = CupsGetDevicesSchema(widget=IPPBodyWidget()) 506 | 507 | cups_get_ppd_schema = CupsGetPPDSchema(widget=IPPBodyWidget()) 508 | 509 | cups_get_ppds_schema = CupsGetPPDsSchema(widget=IPPBodyWidget()) 510 | 511 | cups_get_printers_schema = CupsGetPrintersSchema(widget=IPPBodyWidget()) 512 | 513 | cups_move_job_schema = CupsMoveJobSchema(widget=IPPBodyWidget()) 514 | 515 | cups_reject_jobs_schema = CupsRejectJobsSchema(widget=IPPBodyWidget()) 516 | 517 | get_job_attributes_schema = GetJobAttributesSchema(widget=IPPBodyWidget()) 518 | 519 | get_jobs_schema = GetJobsSchema(widget=IPPBodyWidget()) 520 | 521 | get_printer_attributes_schema = GetPrinterAttributesSchema( 522 | widget=IPPBodyWidget()) 523 | 524 | get_subscriptions_schema = GetSubscriptionsSchema(widget=IPPBodyWidget()) 525 | 526 | get_notifications_schema = GetNotificationsSchema(widget=IPPBodyWidget()) 527 | 528 | pause_printer_schema = PausePrinterSchema(widget=IPPBodyWidget()) 529 | 530 | print_job_schema = PrintJobSchema(widget=IPPBodyWidget()) 531 | 532 | resume_printer_schema = ResumePrinterSchema(widget=IPPBodyWidget()) 533 | 534 | send_document_schema = SendDocumentSchema(widget=IPPBodyWidget()) 535 | 536 | hold_new_jobs_schema = HoldNewJobsSchema(widget=IPPBodyWidget()) 537 | 538 | release_held_new_jobs_schema = ReleaseHeldNewJobsSchema(widget=IPPBodyWidget()) 539 | 540 | cancel_subscription_schema = CancelSubscriptionSchema(widget=IPPBodyWidget()) 541 | -------------------------------------------------------------------------------- /tests/test_highlevel.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import socket 3 | import tempfile 4 | import textwrap 5 | import threading 6 | import time 7 | 8 | from future import standard_library 9 | with standard_library.hooks(): 10 | import http.server 11 | import socketserver 12 | 13 | import mock 14 | import pytest 15 | 16 | import pyipptool 17 | 18 | 19 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 20 | def test_ipptool_create_job_subscription_pull_delivery_method(_call_ipptool): 21 | from pyipptool import create_job_subscription 22 | create_job_subscription( 23 | printer_uri='https://localhost:631/printer/p', 24 | requesting_user_name='admin', 25 | notify_job_id=108, 26 | notify_recipient_uri='rss://', 27 | notify_events=('job-completed', 'job-created', 'job-progress'), 28 | notify_attributes='notify-subscriber-user-name', 29 | notify_charset='utf-8', 30 | notify_natural_language='de', 31 | notify_time_interval=1) 32 | request = _call_ipptool._mock_mock_calls[0][1][-1] 33 | expected_request = textwrap.dedent(""" 34 | { 35 | NAME "Create Job Subscription" 36 | OPERATION "Create-Job-Subscription" 37 | GROUP operation-attributes-tag 38 | ATTR charset attributes-charset utf-8 39 | ATTR language attributes-natural-language en 40 | ATTR uri printer-uri https://localhost:631/printer/p 41 | ATTR name requesting-user-name admin 42 | GROUP subscription-attributes-tag 43 | ATTR uri notify-recipient-uri rss:// 44 | ATTR keyword notify-events job-completed,job-created,job-progress 45 | ATTR integer notify-job-id 108 46 | ATTR keyword notify-attributes notify-subscriber-user-name 47 | ATTR charset notify-charset utf-8 48 | ATTR language notify-natural-language de 49 | ATTR integer notify-time-interval 1 50 | }""").strip() 51 | assert request == expected_request, request 52 | 53 | 54 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 55 | def test_ipptool_create_printer_subscription(_call_ipptool): 56 | from pyipptool import create_printer_subscription 57 | create_printer_subscription( 58 | printer_uri='https://localhost:631/classes/PUBLIC-PDF', 59 | requesting_user_name='admin', 60 | notify_recipient_uri='rss://', 61 | notify_events='all', 62 | notify_attributes='notify-subscriber-user-name', 63 | notify_charset='utf-8', 64 | notify_natural_language='de', 65 | notify_lease_duration=0, 66 | notify_time_interval=1) 67 | request = _call_ipptool._mock_mock_calls[0][1][-1] 68 | expected_request = textwrap.dedent(""" 69 | { 70 | NAME "Create Printer Subscription" 71 | OPERATION "Create-Printer-Subscription" 72 | GROUP operation-attributes-tag 73 | ATTR charset attributes-charset utf-8 74 | ATTR language attributes-natural-language en 75 | ATTR uri printer-uri https://localhost:631/classes/PUBLIC-PDF 76 | ATTR name requesting-user-name admin 77 | GROUP subscription-attributes-tag 78 | ATTR uri notify-recipient-uri rss:// 79 | ATTR keyword notify-events all 80 | ATTR keyword notify-attributes notify-subscriber-user-name 81 | ATTR charset notify-charset utf-8 82 | ATTR language notify-natural-language de 83 | ATTR integer notify-time-interval 1 84 | ATTR integer notify-lease-duration 0 85 | }""").strip() 86 | assert request == expected_request, request 87 | 88 | 89 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 90 | def test_cups_add_modify_printer(_call_ipptool): 91 | from pyipptool import cups_add_modify_printer 92 | _call_ipptool.return_value = {'Tests': [{}]} 93 | cups_add_modify_printer( 94 | printer_uri='https://localhost:631/classes/PUBLIC-PDF', 95 | device_uri='cups-pdf:/', 96 | printer_is_shared=False, 97 | ) 98 | request = _call_ipptool._mock_mock_calls[0][1][-1] 99 | expected_request = textwrap.dedent(""" 100 | { 101 | NAME "CUPS Add Modify Printer" 102 | OPERATION "CUPS-Add-Modify-Printer" 103 | GROUP operation-attributes-tag 104 | ATTR charset attributes-charset utf-8 105 | ATTR language attributes-natural-language en 106 | ATTR uri printer-uri https://localhost:631/classes/PUBLIC-PDF 107 | GROUP printer-attributes-tag 108 | ATTR boolean printer-is-shared 0 109 | ATTR uri device-uri cups-pdf:/ 110 | }""").strip() 111 | assert request == expected_request, request 112 | 113 | 114 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 115 | def test_cups_add_modify_printer_with_ppd(_call_ipptool): 116 | from pyipptool import cups_add_modify_printer 117 | _call_ipptool.return_value = {'Tests': [{}]} 118 | with tempfile.NamedTemporaryFile('rb') as tmp: 119 | cups_add_modify_printer( 120 | printer_uri='https://localhost:631/classes/PUBLIC-PDF', 121 | device_uri='cups-pdf:/', 122 | printer_is_shared=False, 123 | ppd_content=tmp, 124 | ) 125 | request = _call_ipptool._mock_mock_calls[0][1][-1] 126 | expected_request = textwrap.dedent(""" 127 | { 128 | NAME "CUPS Add Modify Printer" 129 | OPERATION "CUPS-Add-Modify-Printer" 130 | GROUP operation-attributes-tag 131 | ATTR charset attributes-charset utf-8 132 | ATTR language attributes-natural-language en 133 | ATTR uri printer-uri https://localhost:631/classes/PUBLIC-PDF 134 | GROUP printer-attributes-tag 135 | ATTR boolean printer-is-shared 0 136 | ATTR uri device-uri cups-pdf:/ 137 | FILE %s 138 | }""" % tmp.name).strip() 139 | assert request == expected_request, request 140 | 141 | 142 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 143 | def test_get_job_attributes_with_job_id(_call_ipptool): 144 | from pyipptool import get_job_attributes 145 | get_job_attributes( 146 | printer_uri='https://localhost:631/classes/PUBLIC-PDF', 147 | job_id=2) 148 | request = _call_ipptool._mock_mock_calls[0][1][0] 149 | expected_request = textwrap.dedent(""" 150 | { 151 | NAME "Get Job Attributes" 152 | OPERATION "Get-Job-Attributes" 153 | GROUP operation-attributes-tag 154 | ATTR charset attributes-charset utf-8 155 | ATTR language attributes-natural-language en 156 | ATTR uri printer-uri https://localhost:631/classes/PUBLIC-PDF 157 | ATTR integer job-id 2 158 | }""").strip() 159 | assert request == expected_request, request 160 | 161 | 162 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 163 | def test_get_job_attributes_with_job_uri(_call_ipptool): 164 | from pyipptool import get_job_attributes 165 | get_job_attributes( 166 | job_uri='https://localhost:631/jobs/2') 167 | request = _call_ipptool._mock_mock_calls[0][1][0] 168 | expected_request = textwrap.dedent(""" 169 | { 170 | NAME "Get Job Attributes" 171 | OPERATION "Get-Job-Attributes" 172 | GROUP operation-attributes-tag 173 | ATTR charset attributes-charset utf-8 174 | ATTR language attributes-natural-language en 175 | ATTR uri job-uri https://localhost:631/jobs/2 176 | }""").strip() 177 | assert request == expected_request, request 178 | 179 | 180 | def test_timeout(): 181 | from pyipptool import wrapper 182 | from pyipptool.core import TimeoutError 183 | from pyipptool.forms import get_subscriptions_form 184 | 185 | class Handler(http.server.BaseHTTPRequestHandler): 186 | """ 187 | HTTP Handler that will make ipptool waiting 188 | """ 189 | 190 | def do_POST(self): 191 | time.sleep(.2) 192 | assassin = threading.Thread(target=self.server.shutdown) 193 | assassin.daemon = True 194 | assassin.start() 195 | 196 | PORT = 6789 197 | while True: 198 | try: 199 | httpd = socketserver.TCPServer(("", PORT), Handler) 200 | except socket.error as exe: 201 | if exe.errno in (48, 98): 202 | PORT += 1 203 | else: 204 | raise 205 | else: 206 | break 207 | httpd.allow_reuse_address = True 208 | 209 | thread = threading.Thread(target=httpd.serve_forever) 210 | thread.daemon = True 211 | thread.start() 212 | 213 | request = get_subscriptions_form.render( 214 | {'header': 215 | {'operation_attributes': 216 | {'printer_uri': 217 | 'http://localhost:%s/printers/fake' % PORT}}}) 218 | 219 | old_timeout = wrapper.config['timeout'] 220 | wrapper.config['timeout'] = .1 221 | wrapper.config['cups_uri'] = 'http://localhost:%s/' % PORT 222 | try: 223 | with pytest.raises(TimeoutError): 224 | wrapper._call_ipptool(request) 225 | finally: 226 | wrapper.config['timeout'] = old_timeout 227 | 228 | 229 | def test_authentication(): 230 | from pyipptool import IPPToolWrapper 231 | wrapper = IPPToolWrapper({'login': 'ezeep', 232 | 'password': 'secret', 233 | 'cups_uri': 'http://localhost:631/' 234 | 'printers/?arg=value'}) 235 | 236 | assert (wrapper.authenticated_uri == 'http://ezeep:secret@localhost:631/' 237 | 'printers/?arg=value') 238 | 239 | 240 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 241 | def test_release_job(_call_ipptool): 242 | from pyipptool import release_job 243 | _call_ipptool.return_value = {'Tests': [{}]} 244 | release_job(job_uri='ipp://cups:631/jobs/3') 245 | request = _call_ipptool._mock_mock_calls[0][1][0] 246 | expected_request = textwrap.dedent(""" 247 | { 248 | NAME "Release Job" 249 | OPERATION "Release-Job" 250 | GROUP operation-attributes-tag 251 | ATTR charset attributes-charset utf-8 252 | ATTR language attributes-natural-language en 253 | ATTR uri job-uri ipp://cups:631/jobs/3 254 | }""").strip() 255 | assert request == expected_request, request 256 | 257 | 258 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 259 | def test_cancel_job(_call_ipptool): 260 | from pyipptool import cancel_job 261 | _call_ipptool.return_value = {'Tests': [{}]} 262 | cancel_job(job_uri='ipp://cups:631/jobs/12') 263 | request = _call_ipptool._mock_mock_calls[0][1][0] 264 | expected_request = textwrap.dedent(""" 265 | { 266 | NAME "Cancel Job" 267 | OPERATION "Cancel-Job" 268 | GROUP operation-attributes-tag 269 | ATTR charset attributes-charset utf-8 270 | ATTR language attributes-natural-language en 271 | ATTR uri job-uri ipp://cups:631/jobs/12 272 | }""").strip() 273 | assert request == expected_request, request 274 | 275 | 276 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 277 | def test_cups_add_modify_class(_call_ipptool): 278 | from pyipptool import cups_add_modify_class 279 | _call_ipptool.return_value = {'Tests': [{}]} 280 | cups_add_modify_class(printer_uri='ipp://cups:631/classes/p', 281 | printer_is_shared=True) 282 | request = _call_ipptool._mock_mock_calls[0][1][0] 283 | expected_request = textwrap.dedent(""" 284 | { 285 | NAME "CUPS Add Modify Class" 286 | OPERATION "CUPS-Add-Modify-Class" 287 | GROUP operation-attributes-tag 288 | ATTR charset attributes-charset utf-8 289 | ATTR language attributes-natural-language en 290 | ATTR uri printer-uri ipp://cups:631/classes/p 291 | GROUP printer-attributes-tag 292 | ATTR boolean printer-is-shared 1 293 | }""").strip() 294 | assert request == expected_request, request 295 | 296 | 297 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 298 | def test_cups_delete_printer(_call_ipptool): 299 | from pyipptool import cups_delete_printer 300 | _call_ipptool.return_value = {'Tests': [{}]} 301 | cups_delete_printer(printer_uri='ipp://cups:631/printers/p') 302 | request = _call_ipptool._mock_mock_calls[0][1][0] 303 | expected_request = textwrap.dedent(""" 304 | { 305 | NAME "CUPS Delete Printer" 306 | OPERATION "CUPS-Delete-Printer" 307 | GROUP operation-attributes-tag 308 | ATTR charset attributes-charset utf-8 309 | ATTR language attributes-natural-language en 310 | ATTR uri printer-uri ipp://cups:631/printers/p 311 | }""").strip() 312 | assert request == expected_request, request 313 | 314 | 315 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 316 | def test_cups_delete_class(_call_ipptool): 317 | from pyipptool import cups_delete_class 318 | _call_ipptool.return_value = {'Tests': [{}]} 319 | cups_delete_class(printer_uri='ipp://cups:631/classes/p') 320 | request = _call_ipptool._mock_mock_calls[0][1][0] 321 | expected_request = textwrap.dedent(""" 322 | { 323 | NAME "CUPS Delete Class" 324 | OPERATION "CUPS-Delete-Class" 325 | GROUP operation-attributes-tag 326 | ATTR charset attributes-charset utf-8 327 | ATTR language attributes-natural-language en 328 | ATTR uri printer-uri ipp://cups:631/classes/p 329 | }""").strip() 330 | assert request == expected_request, request 331 | 332 | 333 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 334 | def test_cups_get_classes(_call_ipptool): 335 | from pyipptool import cups_get_classes 336 | _call_ipptool.return_value = {'Tests': [{}]} 337 | cups_get_classes() 338 | request = _call_ipptool._mock_mock_calls[0][1][0] 339 | expected_request = textwrap.dedent(""" 340 | { 341 | NAME "CUPS Get Classes" 342 | OPERATION "CUPS-Get-Classes" 343 | GROUP operation-attributes-tag 344 | ATTR charset attributes-charset utf-8 345 | ATTR language attributes-natural-language en 346 | }""").strip() 347 | assert request == expected_request, request 348 | 349 | 350 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 351 | def test_cups_get_printers(_call_ipptool): 352 | from pyipptool import cups_get_printers 353 | _call_ipptool.return_value = {'Tests': [{}]} 354 | cups_get_printers() 355 | request = _call_ipptool._mock_mock_calls[0][1][0] 356 | expected_request = textwrap.dedent(""" 357 | { 358 | NAME "CUPS Get Printers" 359 | OPERATION "CUPS-Get-Printers" 360 | GROUP operation-attributes-tag 361 | ATTR charset attributes-charset utf-8 362 | ATTR language attributes-natural-language en 363 | }""").strip() 364 | assert request == expected_request, request 365 | 366 | 367 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 368 | def test_cups_get_devices(_call_ipptool): 369 | from pyipptool import cups_get_devices 370 | _call_ipptool.return_value = {'Tests': [{}]} 371 | cups_get_devices() 372 | request = _call_ipptool._mock_mock_calls[0][1][0] 373 | expected_request = textwrap.dedent(""" 374 | { 375 | NAME "CUPS Get Devices" 376 | OPERATION "CUPS-Get-Devices" 377 | GROUP operation-attributes-tag 378 | ATTR charset attributes-charset utf-8 379 | ATTR language attributes-natural-language en 380 | }""").strip() 381 | assert request == expected_request, request 382 | 383 | 384 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 385 | def test_cups_get_ppd_with_printer_uri(_call_ipptool): 386 | from pyipptool import cups_get_ppd 387 | _call_ipptool.return_value = {'Tests': [{}]} 388 | cups_get_ppd(printer_uri='ipp://cups:631/printers/p') 389 | request = _call_ipptool._mock_mock_calls[0][1][0] 390 | expected_request = textwrap.dedent(""" 391 | { 392 | NAME "CUPS Get PPD" 393 | OPERATION "CUPS-Get-PPD" 394 | GROUP operation-attributes-tag 395 | ATTR charset attributes-charset utf-8 396 | ATTR language attributes-natural-language en 397 | ATTR uri printer-uri ipp://cups:631/printers/p 398 | }""").strip() 399 | assert request == expected_request, request 400 | 401 | 402 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 403 | def test_cups_get_ppd_with_ppd_name(_call_ipptool): 404 | from pyipptool import cups_get_ppd 405 | _call_ipptool.return_value = {'Tests': [{}]} 406 | cups_get_ppd(ppd_name='ppd-for-my-printer') 407 | request = _call_ipptool._mock_mock_calls[0][1][0] 408 | expected_request = textwrap.dedent(""" 409 | { 410 | NAME "CUPS Get PPD" 411 | OPERATION "CUPS-Get-PPD" 412 | GROUP operation-attributes-tag 413 | ATTR charset attributes-charset utf-8 414 | ATTR language attributes-natural-language en 415 | ATTR name ppd-name ppd-for-my-printer 416 | }""").strip() 417 | assert request == expected_request, request 418 | 419 | 420 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 421 | def test_cups_get_ppds(_call_ipptool): 422 | from pyipptool import cups_get_ppds 423 | _call_ipptool.return_value = {'Tests': [{}]} 424 | cups_get_ppds() 425 | request = _call_ipptool._mock_mock_calls[0][1][0] 426 | expected_request = textwrap.dedent(""" 427 | { 428 | NAME "CUPS Get PPDs" 429 | OPERATION "CUPS-Get-PPDs" 430 | GROUP operation-attributes-tag 431 | ATTR charset attributes-charset utf-8 432 | ATTR language attributes-natural-language en 433 | }""").strip() 434 | assert request == expected_request, request 435 | 436 | 437 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 438 | def test_cups_move_job(_call_ipptool): 439 | from pyipptool import cups_move_job 440 | _call_ipptool.return_value = {'Tests': [{}]} 441 | cups_move_job(job_uri='ipp://cups:631/jobs/12', 442 | job_printer_uri='ipp://cups:631/printers/p') 443 | request = _call_ipptool._mock_mock_calls[0][1][0] 444 | expected_request = textwrap.dedent(""" 445 | { 446 | NAME "CUPS Move Job" 447 | OPERATION "CUPS-Move-Job" 448 | GROUP operation-attributes-tag 449 | ATTR charset attributes-charset utf-8 450 | ATTR language attributes-natural-language en 451 | ATTR uri job-uri ipp://cups:631/jobs/12 452 | GROUP job-attributes-tag 453 | ATTR uri job-printer-uri ipp://cups:631/printers/p 454 | }""").strip() 455 | assert request == expected_request, request 456 | 457 | 458 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 459 | def test_cups_reject_jobs(_call_ipptool): 460 | from pyipptool import cups_reject_jobs 461 | _call_ipptool.return_value = {'Tests': [{}]} 462 | cups_reject_jobs(printer_uri='ipp://cups:631/printers/p', 463 | requesting_user_name='boby') 464 | request = _call_ipptool._mock_mock_calls[0][1][0] 465 | expected_request = textwrap.dedent(""" 466 | { 467 | NAME "CUPS Reject Jobs" 468 | OPERATION "CUPS-Reject-Jobs" 469 | GROUP operation-attributes-tag 470 | ATTR charset attributes-charset utf-8 471 | ATTR language attributes-natural-language en 472 | ATTR uri printer-uri ipp://cups:631/printers/p 473 | ATTR name requesting-user-name boby 474 | GROUP printer-attributes-tag 475 | }""").strip() 476 | assert request == expected_request, request 477 | 478 | 479 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 480 | def test_get_jobs(_call_ipptool): 481 | from pyipptool import get_jobs 482 | _call_ipptool.return_value = {'Tests': [{}]} 483 | get_jobs(printer_uri='ipp://cups:631/printers/p') 484 | request = _call_ipptool._mock_mock_calls[0][1][0] 485 | expected_request = textwrap.dedent(""" 486 | { 487 | NAME "Get Jobs" 488 | OPERATION "Get-Jobs" 489 | GROUP operation-attributes-tag 490 | ATTR charset attributes-charset utf-8 491 | ATTR language attributes-natural-language en 492 | ATTR uri printer-uri ipp://cups:631/printers/p 493 | }""").strip() 494 | assert request == expected_request, request 495 | 496 | 497 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 498 | def test_get_printer_attributes(_call_ipptool): 499 | from pyipptool import get_printer_attributes 500 | _call_ipptool.return_value = {'Tests': [{}]} 501 | get_printer_attributes(printer_uri='ipp://cups:631/printers/p') 502 | request = _call_ipptool._mock_mock_calls[0][1][0] 503 | expected_request = textwrap.dedent(""" 504 | { 505 | NAME "Get Printer Attributes" 506 | OPERATION "Get-Printer-Attributes" 507 | GROUP operation-attributes-tag 508 | ATTR charset attributes-charset utf-8 509 | ATTR language attributes-natural-language en 510 | ATTR uri printer-uri ipp://cups:631/printers/p 511 | }""").strip() 512 | assert request == expected_request, request 513 | 514 | 515 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 516 | def test_get_subscriptions(_call_ipptool): 517 | from pyipptool import get_subscriptions 518 | _call_ipptool.return_value = {'Tests': [{}]} 519 | get_subscriptions(printer_uri='ipp://cups:631/printers/p') 520 | request = _call_ipptool._mock_mock_calls[0][1][0] 521 | expected_request = textwrap.dedent(""" 522 | { 523 | NAME "Get Subscriptions" 524 | OPERATION "Get-Subscriptions" 525 | GROUP operation-attributes-tag 526 | ATTR charset attributes-charset utf-8 527 | ATTR language attributes-natural-language en 528 | ATTR uri printer-uri ipp://cups:631/printers/p 529 | }""").strip() 530 | assert request == expected_request, request 531 | 532 | 533 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 534 | def test_get_notifications(_call_ipptool): 535 | from pyipptool import get_notifications 536 | _call_ipptool.return_value = {'Tests': [{}]} 537 | get_notifications(printer_uri='ipp://cups:631/printers/p', 538 | notify_subscription_ids=3) 539 | request = _call_ipptool._mock_mock_calls[0][1][0] 540 | expected_request = textwrap.dedent(""" 541 | { 542 | NAME "Get Notifications" 543 | OPERATION "Get-Notifications" 544 | GROUP operation-attributes-tag 545 | ATTR charset attributes-charset utf-8 546 | ATTR language attributes-natural-language en 547 | ATTR uri printer-uri ipp://cups:631/printers/p 548 | ATTR integer notify-subscription-ids 3 549 | }""").strip() 550 | assert request == expected_request, request 551 | 552 | 553 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 554 | def test_pause_printer(_call_ipptool): 555 | from pyipptool import pause_printer 556 | _call_ipptool.return_value = {'Tests': [{}]} 557 | pause_printer(printer_uri='ipp://cups:631/printers/p') 558 | request = _call_ipptool._mock_mock_calls[0][1][0] 559 | expected_request = textwrap.dedent(""" 560 | { 561 | NAME "Pause Printer" 562 | OPERATION "Pause-Printer" 563 | GROUP operation-attributes-tag 564 | ATTR charset attributes-charset utf-8 565 | ATTR language attributes-natural-language en 566 | ATTR uri printer-uri ipp://cups:631/printers/p 567 | }""").strip() 568 | assert request == expected_request, request 569 | 570 | 571 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 572 | def test_hold_new_jobs(_call_ipptool): 573 | from pyipptool import hold_new_jobs 574 | _call_ipptool.return_value = {'Tests': [{}]} 575 | hold_new_jobs(printer_uri='ipp://cups:631/printers/p') 576 | request = _call_ipptool._mock_mock_calls[0][1][0] 577 | expected_request = textwrap.dedent(""" 578 | { 579 | NAME "Hold New Jobs" 580 | OPERATION "Hold-New-Jobs" 581 | GROUP operation-attributes-tag 582 | ATTR charset attributes-charset utf-8 583 | ATTR language attributes-natural-language en 584 | ATTR uri printer-uri ipp://cups:631/printers/p 585 | }""").strip() 586 | assert request == expected_request, request 587 | 588 | 589 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 590 | def test_release_held_new_jobs(_call_ipptool): 591 | from pyipptool import release_held_new_jobs 592 | _call_ipptool.return_value = {'Tests': [{}]} 593 | release_held_new_jobs(printer_uri='ipp://cups:631/printers/p') 594 | request = _call_ipptool._mock_mock_calls[0][1][0] 595 | expected_request = textwrap.dedent(""" 596 | { 597 | NAME "Release Held New Jobs" 598 | OPERATION "Release-Held-New-Jobs" 599 | GROUP operation-attributes-tag 600 | ATTR charset attributes-charset utf-8 601 | ATTR language attributes-natural-language en 602 | ATTR uri printer-uri ipp://cups:631/printers/p 603 | }""").strip() 604 | assert request == expected_request, request 605 | 606 | 607 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 608 | def test_resume_printer(_call_ipptool): 609 | from pyipptool import resume_printer 610 | _call_ipptool.return_value = {'Tests': [{}]} 611 | resume_printer(printer_uri='ipp://cups:631/printers/p') 612 | request = _call_ipptool._mock_mock_calls[0][1][0] 613 | expected_request = textwrap.dedent(""" 614 | { 615 | NAME "Resume Printer" 616 | OPERATION "Resume-Printer" 617 | GROUP operation-attributes-tag 618 | ATTR charset attributes-charset utf-8 619 | ATTR language attributes-natural-language en 620 | ATTR uri printer-uri ipp://cups:631/printers/p 621 | }""").strip() 622 | assert request == expected_request, request 623 | 624 | 625 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 626 | def test_cancel_subscription(_call_ipptool): 627 | from pyipptool import cancel_subscription 628 | _call_ipptool.return_value = {'Tests': [{}]} 629 | cancel_subscription(printer_uri='ipp://cups:631/printers/p', 630 | notify_subscription_id=3) 631 | request = _call_ipptool._mock_mock_calls[0][1][0] 632 | expected_request = textwrap.dedent(""" 633 | { 634 | NAME "Cancel Subscription" 635 | OPERATION "Cancel-Subscription" 636 | GROUP operation-attributes-tag 637 | ATTR charset attributes-charset utf-8 638 | ATTR language attributes-natural-language en 639 | ATTR uri printer-uri ipp://cups:631/printers/p 640 | ATTR integer notify-subscription-id 3 641 | }""").strip() 642 | assert request == expected_request, request 643 | 644 | 645 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 646 | def test_create_job(_call_ipptool): 647 | from pyipptool import create_job 648 | _call_ipptool.return_value = {'Tests': [{}]} 649 | create_job(printer_uri='ipp://cups:631/classes/p', 650 | job_name='foo', 651 | job_priority=1, 652 | job_hold_until='indefinite', 653 | job_sheets='standard', 654 | multiple_document_handling='single-document', 655 | copies=2, 656 | finishings='punch', 657 | page_ranges='1-6', 658 | sides='two-sided-short-edge', 659 | number_up=4, 660 | orientation_requested='reverse-landscape', 661 | media='iso-a4-white', 662 | printer_resolution='600dpi', 663 | print_quality='5', 664 | ipp_attribute_fidelity=False, 665 | job_k_octets=1024, 666 | job_impressions=2048, 667 | job_media_sheets=2, 668 | auth_info='michael', 669 | job_billing='no-idea', 670 | ) 671 | request = _call_ipptool._mock_mock_calls[0][1][-1] 672 | expected_request = textwrap.dedent(""" 673 | { 674 | NAME "Create Job" 675 | OPERATION "Create-Job" 676 | GROUP operation-attributes-tag 677 | ATTR charset attributes-charset utf-8 678 | ATTR language attributes-natural-language en 679 | ATTR uri printer-uri ipp://cups:631/classes/p 680 | ATTR name job-name foo 681 | ATTR boolean ipp-attribute-fidelity 0 682 | ATTR integer job-k-octets 1024 683 | ATTR integer job-impressions 2048 684 | ATTR integer job-media-sheets 2 685 | GROUP job-attributes-tag 686 | ATTR integer job-priority 1 687 | ATTR keyword job-hold-until indefinite 688 | ATTR keyword job-sheets standard 689 | ATTR text auth-info "michael" 690 | ATTR text job-billing "no-idea" 691 | ATTR keyword multiple-document-handling single-document 692 | ATTR integer copies 2 693 | ATTR enum finishings punch 694 | ATTR rangeOfInteger page-ranges 1-6 695 | ATTR keyword sides two-sided-short-edge 696 | ATTR integer number-up 4 697 | ATTR enum orientation-requested reverse-landscape 698 | ATTR keyword media iso-a4-white 699 | ATTR resolution printer-resolution 600dpi 700 | ATTR enum print-quality 5 701 | }""").strip() 702 | assert request == expected_request, request 703 | 704 | 705 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 706 | def test_print_job(_call_ipptool): 707 | from pyipptool import print_job 708 | _call_ipptool.return_value = {'Tests': [{}]} 709 | filename = os.path.join(os.path.dirname(__file__), 'hello.pdf') 710 | with open(filename, 'rb') as tmp: 711 | print_job(printer_uri='ipp://cups:631/classes/p', 712 | job_name='foo', 713 | requesting_user_name='john-rambo', 714 | ipp_attribute_fidelity=False, 715 | document_name='foo.txt', 716 | compression='gzip', 717 | document_format='text/plain', 718 | document_natural_language='en', 719 | job_k_octets=1024, 720 | job_impressions=2048, 721 | job_media_sheets=2, 722 | job_priority=1, 723 | job_hold_until='indefinite', 724 | job_sheets='standard', 725 | auth_info='michael', 726 | job_billing='no-idea', 727 | multiple_document_handling='single-document', 728 | copies=2, 729 | finishings='punch', 730 | page_ranges='1-6', 731 | sides='two-sided-short-edge', 732 | number_up=4, 733 | orientation_requested='reverse-landscape', 734 | media='iso-a4-white', 735 | printer_resolution='600dpi', 736 | print_quality='5', 737 | document_content=tmp.read(), 738 | ezeep_job_uuid='bla') 739 | request = _call_ipptool._mock_mock_calls[0][1][-1] 740 | expected_request = textwrap.dedent(""" 741 | { 742 | NAME "Print Job" 743 | OPERATION "Print-Job" 744 | GROUP operation-attributes-tag 745 | ATTR charset attributes-charset utf-8 746 | ATTR language attributes-natural-language en 747 | ATTR uri printer-uri ipp://cups:631/classes/p 748 | ATTR name requesting-user-name john-rambo 749 | ATTR name job-name foo 750 | ATTR boolean ipp-attribute-fidelity 0 751 | ATTR integer job-k-octets 1024 752 | ATTR integer job-impressions 2048 753 | ATTR integer job-media-sheets 2 754 | ATTR name document-name foo.txt 755 | ATTR keyword compression gzip 756 | ATTR mimeMediaType document-format text/plain 757 | ATTR naturalLanguage document-natural-language en 758 | GROUP job-attributes-tag 759 | ATTR integer job-priority 1 760 | ATTR keyword job-hold-until indefinite 761 | ATTR keyword job-sheets standard 762 | ATTR text auth-info "michael" 763 | ATTR text job-billing "no-idea" 764 | ATTR keyword multiple-document-handling single-document 765 | ATTR integer copies 2 766 | ATTR enum finishings punch 767 | ATTR rangeOfInteger page-ranges 1-6 768 | ATTR keyword sides two-sided-short-edge 769 | ATTR integer number-up 4 770 | ATTR enum orientation-requested reverse-landscape 771 | ATTR keyword media iso-a4-white 772 | ATTR resolution printer-resolution 600dpi 773 | ATTR enum print-quality 5 774 | ATTR text ezeep-job-uuid "bla" 775 | GROUP subscription-attributes-tag 776 | GROUP document-attributes-tag 777 | FILE /tmp/ 778 | }""").strip() 779 | assert ('\n'.join(request.splitlines()[:-2]) 780 | == '\n'.join(expected_request.splitlines()[:-2])), request 781 | assert expected_request.splitlines()[-2].startswith('FILE /tmp/') 782 | 783 | 784 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 785 | def test_send_document_with_file(_call_ipptool): 786 | from pyipptool import send_document 787 | _call_ipptool.return_value = {'Tests': [{}]} 788 | 789 | with tempfile.NamedTemporaryFile('rb') as tmp: 790 | send_document(printer_uri='ipp://cups:631/printers/p', 791 | requesting_user_name='you', 792 | document_content=tmp) 793 | request = _call_ipptool._mock_mock_calls[0][1][-1] 794 | expected_request = textwrap.dedent(""" 795 | { 796 | NAME "Send Document" 797 | OPERATION "Send-Document" 798 | GROUP operation-attributes-tag 799 | ATTR charset attributes-charset utf-8 800 | ATTR language attributes-natural-language en 801 | ATTR uri printer-uri ipp://cups:631/printers/p 802 | ATTR name requesting-user-name you 803 | ATTR mimeMediaType document-format application/pdf 804 | ATTR boolean last-document 1 805 | GROUP document-attributes-tag 806 | FILE %s 807 | }""" % tmp.name).strip() 808 | assert request == expected_request 809 | 810 | 811 | @mock.patch.object(pyipptool.wrapper, '_call_ipptool') 812 | def test_send_document_with_binary(_call_ipptool): 813 | from pyipptool import send_document 814 | _call_ipptool.return_value = {'Tests': [{}]} 815 | 816 | with open(os.path.join(os.path.dirname(__file__), 817 | 'hello.pdf'), 'rb') as tmp: 818 | send_document(document_content=tmp.read()) 819 | assert 'FILE /tmp/' in _call_ipptool._mock_mock_calls[0][1][-1] 820 | -------------------------------------------------------------------------------- /tests/test_form.py: -------------------------------------------------------------------------------- 1 | import colander 2 | import mock 3 | import pytest 4 | 5 | 6 | def test_cancel_job_form(): 7 | from pyipptool.forms import cancel_job_form 8 | request = cancel_job_form.render( 9 | {'operation_attributes_tag': 10 | {'printer_uri': 'https://localhost:631/classes/PIY', 11 | 'job_id': 8, 12 | 'job_uri': 'https://localhost:631/jobs/8', 13 | 'purge_job': True}}) 14 | assert 'NAME "Cancel Job"' in request 15 | assert 'OPERATION "Cancel-Job"' in request 16 | assert 'ATTR uri printer-uri https://localhost:631/classes/PIY' in request 17 | assert 'ATTR integer job-id 8' in request 18 | assert 'ATTR uri job-uri https://localhost:631/jobs/8' in request 19 | assert 'ATTR boolean purge-job 1' in request 20 | 21 | 22 | def test_release_job_form_with_job_id(): 23 | from pyipptool.forms import release_job_form 24 | request = release_job_form.render( 25 | {'operation_attributes_tag': 26 | {'printer_uri': 'https://localhost:631/classes/PIY', 27 | 'job_id': 7}}) 28 | assert 'NAME "Release Job"' in request 29 | assert 'OPERATION "Release-Job"' in request 30 | assert 'ATTR uri printer-uri https://localhost:631/classes/PIY' in request 31 | assert 'ATTR integer job-id 7' in request 32 | 33 | 34 | def test_release_job_form_with_job_uri(): 35 | from pyipptool.forms import release_job_form 36 | request = release_job_form.render( 37 | {'operation_attributes_tag': 38 | {'job_uri': 'https://localhost:631/jobs/7'}}) 39 | assert 'NAME "Release Job"' in request 40 | assert 'OPERATION "Release-Job"' in request 41 | assert 'ATTR uri job-uri https://localhost:631/jobs/7' in request 42 | 43 | 44 | def test_create_printer_subscription_form(): 45 | from pyipptool.forms import create_printer_subscription_form 46 | request = create_printer_subscription_form.render( 47 | {'operation_attributes_tag': 48 | {'printer_uri': 'https://localhost:631/classes/PIY', 49 | 'requesting_user_name': 'admin'}, 50 | 'subscription_attributes_tag': 51 | {'notify_recipient_uri': 'rss://', 52 | 'notify_events': 'all', 53 | 'notify_attributes': 'notify-subscriber-user-name', 54 | 'notify_charset': 'utf-8', 55 | 'notify_natural_language': 'de', 56 | 'notify_lease_duration': 128, 57 | 'notify_time_interval': 1}}) 58 | assert 'NAME "Create Printer Subscription"' in request, request 59 | assert 'OPERATION "Create-Printer-Subscription"' in request, request 60 | assert 'ATTR charset attributes-charset utf-8' in request, request 61 | assert 'ATTR language attributes-natural-language en' in request, request 62 | assert 'ATTR name requesting-user-name admin' in request, request 63 | assert 'GROUP subscription-attributes-tag' in request 64 | assert 'ATTR uri printer-uri https://localhost:631/classes/PIY' in request 65 | assert 'ATTR uri notify-recipient-uri rss://' in request 66 | assert 'ATTR keyword notify-events all' in request 67 | assert 'ATTR charset notify-charset utf-8' in request 68 | assert 'ATTR language notify-natural-language de' in request 69 | assert 'ATTR integer notify-lease-duration 128' in request 70 | assert 'ATTR integer notify-time-interval 1' in request 71 | 72 | 73 | def test_create_job_subscription_form_for_pull_delivery_method(): 74 | from pyipptool.forms import create_job_subscription_form 75 | request = create_job_subscription_form.render( 76 | {'operation_attributes_tag': 77 | {'printer_uri': 'https://localhost:631/printer/p', 78 | 'requesting_user_name': 'admin'}, 79 | 'subscription_attributes_tag': 80 | {'notify_recipient_uri': 'rss://', 81 | 'notify_job_id': 12, 82 | 'notify_events': ('job-completed', 'job-created', 'job-progress'), 83 | 'notify_attributes': 'notify-subscriber-user-name', 84 | 'notify_charset': 'utf-8', 85 | 'notify_natural_language': 'de', 86 | 'notify_time_interval': 1}}) 87 | assert 'NAME "Create Job Subscription"' in request 88 | assert 'OPERATION "Create-Job-Subscription"' in request 89 | 90 | assert 'GROUP operation-attributes-tag' in request, request 91 | assert 'ATTR charset attributes-charset utf-8' in request 92 | assert 'ATTR language attributes-natural-language en' in request 93 | assert 'ATTR name requesting-user-name admin' in request 94 | 95 | assert 'GROUP subscription-attributes-tag' in request 96 | assert 'ATTR uri printer-uri https://localhost:631/printer/p' in request 97 | assert 'ATTR integer notify-job-id 12' in request, request 98 | assert 'ATTR uri notify-recipient-uri rss://' in request 99 | assert ('ATTR keyword notify-events job-completed,job-created,job-progress' 100 | in request), request 101 | assert 'ATTR charset notify-charset utf-8' in request 102 | assert 'ATTR language notify-natural-language de' in request 103 | assert 'ATTR integer notify-time-interval 1' in request 104 | 105 | 106 | def test_cups_add_modify_class_form(): 107 | from pyipptool.forms import cups_add_modify_class_form 108 | m_uri_0 = 'ipp://localhost:631/printers/p0' 109 | m_uri_1 = 'ipp://localhost:631/classes/c0' 110 | request = cups_add_modify_class_form.render( 111 | {'operation_attributes_tag': 112 | {'printer_uri': 'https://localhost:631/printers/p0'}, 113 | 'printer_attributes_tag': 114 | {'auth_info_required': 'john', 115 | 'member_uris': (m_uri_0, m_uri_1), 116 | 'printer_is_accepting_jobs': True, 117 | 'printer_info': 'multiline\ntext', 118 | 'printer_location': 'The Office', 119 | 'printer_more_info': 'http://example.com', 120 | 'printer_op_policy': 'brain', 121 | 'printer_state': '3', 122 | 'printer_state_message': 'Ready to print', 123 | 'requesting_user_name_allowed': 'me', 124 | 'printer_is_shared': False}}) 125 | assert 'NAME "CUPS Add Modify Class"' 126 | assert 'OPERATION "CUPS-Add-Modify-Class"' in request 127 | assert 'GROUP operation-attributes-tag' in request, request 128 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 129 | 130 | assert 'GROUP printer-attributes-tag' in request 131 | assert 'ATTR uri member-uris %s,%s' % (m_uri_0, m_uri_1) in request 132 | assert 'ATTR keyword auth-info-required john' in request, request 133 | assert 'ATTR boolean printer-is-accepting-jobs 1' in request, request 134 | assert 'ATTR text printer-info "multiline\ntext"' in request 135 | assert 'ATTR text printer-location "The Office"' in request 136 | assert 'ATTR uri printer-more-info http://example.com' in request 137 | assert 'ATTR name printer-op-policy brain' in request 138 | assert 'ATTR enum printer-state 3' in request, request 139 | assert 'ATTR text printer-state-message "Ready to print"' in request 140 | assert 'ATTR name requesting-user-name-allowed me' in request 141 | assert 'ATTR boolean printer-is-shared 0' in request 142 | 143 | 144 | def test_cups_add_modify_printer_form(): 145 | from pyipptool.forms import cups_add_modify_printer_form 146 | request = cups_add_modify_printer_form.render( 147 | {'operation_attributes_tag': 148 | {'printer_uri': 'https://localhost:631/printers/p0'}, 149 | 'printer_attributes_tag': 150 | {'device_uri': 'cups-pdf:/', 151 | 'auth_info_required': 'john', 152 | 'job_sheets_default': 'none', 153 | 'port_monitor': 'port', 154 | 'ppd_name': 'printer.ppd', 155 | 'printer_is_accepting_jobs': True, 156 | 'printer_info': 'multiline\ntext', 157 | 'printer_location': 'The Office', 158 | 'printer_more_info': 'http://example.com', 159 | 'printer_op_policy': 'pinky', 160 | 'printer_state': '3', 161 | 'printer_state_message': 'Ready to print', 162 | 'requesting_user_name_allowed': 'me', 163 | 'printer_is_shared': True}}) 164 | assert 'NAME "CUPS Add Modify Printer"' 165 | assert 'OPERATION "CUPS-Add-Modify-Printer"' in request 166 | assert 'GROUP operation-attributes-tag' in request 167 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 168 | 169 | assert 'GROUP printer-attributes-tag' in request 170 | assert 'ATTR uri device-uri cups-pdf:/' in request 171 | assert 'ATTR keyword auth-info-required john' in request 172 | assert 'ATTR name job-sheets-default none' in request 173 | assert 'ATTR name port-monitor port' in request 174 | assert 'ATTR name ppd-name printer.ppd' in request 175 | assert 'ATTR boolean printer-is-accepting-jobs 1' in request, request 176 | assert 'ATTR text printer-info "multiline\ntext"' in request 177 | assert 'ATTR text printer-location "The Office"' in request 178 | assert 'ATTR uri printer-more-info http://example.com' in request 179 | assert 'ATTR name printer-op-policy pinky' in request 180 | assert 'ATTR enum printer-state 3' in request 181 | assert 'ATTR text printer-state-message "Ready to print"' in request 182 | assert 'ATTR name requesting-user-name-allowed me' in request 183 | assert 'ATTR boolean printer-is-shared 1' in request 184 | 185 | 186 | def test_cups_add_modify_printer_form_with_None(): 187 | from pyipptool.forms import cups_add_modify_printer_form 188 | with pytest.raises(ValueError) as exec_info: 189 | cups_add_modify_printer_form.render( 190 | {'operation_attributes_tag': 191 | {'printer_uri': 'https://localhost:631/printers/p0'}, 192 | 'printer_attributes_tag': {'printer_state_message': None}}) 193 | assert str(exec_info.value) == ("None value provided for" 194 | " 'printer_state_message'") 195 | 196 | 197 | def test_cups_delete_printer_form(): 198 | from pyipptool.forms import cups_delete_printer_form 199 | request = cups_delete_printer_form.render( 200 | {'operation_attributes_tag': 201 | {'printer_uri': 'https://localhost:631/printers/p0'}}) 202 | assert 'NAME "CUPS Delete Printer"' in request 203 | assert 'OPERATION "CUPS-Delete-Printer"' in request 204 | assert 'GROUP operation-attributes-tag' in request 205 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 206 | 207 | 208 | def test_cups_delete_class_form(): 209 | from pyipptool.forms import cups_delete_class_form 210 | request = cups_delete_class_form.render( 211 | {'operation_attributes_tag': 212 | {'printer_uri': 'https://localhost:631/classes/p0'}}) 213 | assert 'NAME "CUPS Delete Class"' in request 214 | assert 'OPERATION "CUPS-Delete-Class"' in request 215 | assert 'GROUP operation-attributes-tag' in request 216 | assert 'ATTR uri printer-uri https://localhost:631/classes/p0' in request 217 | 218 | 219 | def test_cups_get_classes_form(): 220 | from pyipptool.forms import cups_get_classes_form 221 | request = cups_get_classes_form.render( 222 | {'operation_attributes_tag': 223 | {'first_printer_name': 'DA-Printer', 224 | 'limit': 2, 225 | 'printer_location': 'The Office', 226 | 'printer_type': '2', 227 | 'printer_type_mask': '8', 228 | 'requested_attributes': ('name', 'printer-attributes-tag'), 229 | 'requested_user_name': 'john'}}) 230 | assert 'NAME "CUPS Get Classes"' in request, request 231 | assert 'OPERATION "CUPS-Get-Classes"' in request, request 232 | assert 'ATTR name first-printer-name DA-Printer' in request, request 233 | assert 'ATTR integer limit 2' in request 234 | assert 'ATTR text printer-location "The Office"' in request, request 235 | assert 'ATTR enum printer-type 2' in request 236 | assert 'ATTR enum printer-type-mask 8' in request 237 | assert ('ATTR keyword requested-attributes' 238 | ' name,printer-attributes-tag' in request), request 239 | assert 'ATTR name requested-user-name john' in request 240 | 241 | 242 | def test_cups_get_devices_form(): 243 | from pyipptool.forms import cups_get_devices_form 244 | request = cups_get_devices_form.render( 245 | {'operation_attributes_tag': 246 | {'device_class': 'fermionic', 247 | 'exclude_schemes': 'foo', 248 | 'include_schemes': 'bar', 249 | 'limit': 3, 250 | 'requested_attributes': 'all', 251 | 'timeout': 12}}) 252 | assert 'NAME "CUPS Get Devices"' in request 253 | assert 'OPERATION "CUPS-Get-Devices"' in request 254 | assert 'ATTR keyword device-class fermionic' in request 255 | assert 'ATTR name exclude-schemes foo' in request 256 | assert 'ATTR name include-schemes bar' in request 257 | assert 'ATTR integer limit 3' in request 258 | assert 'ATTR keyword requested-attributes all' in request 259 | assert 'ATTR integer timeout 12' in request 260 | 261 | 262 | def test_cups_get_ppds_form(): 263 | from pyipptool.forms import cups_get_ppds_form 264 | request = cups_get_ppds_form.render( 265 | {'operation_attributes_tag': 266 | {'exclude_schemes': 'foo', 267 | 'include_schemes': 'bar', 268 | 'limit': 3, 269 | 'ppd_make': 'Manufaktur', 270 | 'ppd_make_and_model': 'Manufaktur XYZ', 271 | 'ppd_model_number': '1234', 272 | 'ppd_natural_language': 'en', 273 | 'ppd_product': 'Generic', 274 | 'ppd_psversion': 'PS3', 275 | 'ppd_type': 'generic', 276 | 'requested_attributes': 'all'}}) 277 | assert 'NAME "CUPS Get PPDs"' in request 278 | assert 'OPERATION "CUPS-Get-PPDs"' in request 279 | assert 'ATTR name exclude-schemes foo' in request 280 | assert 'ATTR name include-schemes bar' in request 281 | assert 'ATTR text ppd-make "Manufaktur"' in request 282 | assert 'ATTR text ppd-make-and-model "Manufaktur XYZ"' in request 283 | assert 'ATTR integer ppd-model-number 1234' in request 284 | assert 'ATTR naturalLanguage ppd-natural-language en' in request 285 | assert 'ATTR text ppd-product "Generic"' in request 286 | assert 'ATTR text ppd-psversion "PS3"' in request 287 | assert 'ATTR keyword ppd-type generic' in request 288 | assert 'ATTR keyword requested-attributes all' in request 289 | assert 'ATTR integer limit 3' in request 290 | 291 | 292 | def test_cups_get_printers_form(): 293 | from pyipptool.forms import cups_get_printers_form 294 | request = cups_get_printers_form.render( 295 | {'operation_attributes_tag': 296 | {'first_printer_name': 'DA-Printer', 297 | 'limit': 2, 298 | 'printer_location': 'The Office', 299 | 'printer_type': '2', 300 | 'printer_type_mask': '8', 301 | 'requested_attributes': ('name', 'printer-attributes-tag'), 302 | 'requested_user_name': 'john'}}) 303 | assert 'NAME "CUPS Get Printers"' in request, request 304 | assert 'OPERATION "CUPS-Get-Printers"' in request, request 305 | assert 'ATTR name first-printer-name DA-Printer' in request, request 306 | assert 'ATTR integer limit 2' in request 307 | assert 'ATTR text printer-location "The Office"' in request, request 308 | assert 'ATTR enum printer-type 2' in request 309 | assert 'ATTR enum printer-type-mask 8' in request 310 | assert ('ATTR keyword requested-attributes' 311 | ' name,printer-attributes-tag' in request), request 312 | assert 'ATTR name requested-user-name john' in request 313 | 314 | 315 | def test_cups_reject_jobs_form(): 316 | from pyipptool.forms import cups_reject_jobs_form 317 | request = cups_reject_jobs_form.render( 318 | {'operation_attributes_tag': 319 | {'printer_uri': 'ipp://cups:631/printers/p', 320 | 'requesting_user_name': 'admin'}, 321 | 'printer_attributes_tag': 322 | {'printer_state_message': 'You shall not pass'}}) 323 | assert 'NAME "CUPS Reject Jobs"' in request, request 324 | assert 'OPERATION "CUPS-Reject-Jobs"' in request, request 325 | 326 | assert 'GROUP operation-attributes-tag' in request 327 | assert 'ATTR uri printer-uri ipp://cups:631/printers/p' in request 328 | 329 | assert 'GROUP printer-attributes-tag' in request 330 | assert 'ATTR text printer-state-message "You shall not pass"' in request 331 | 332 | 333 | def test_get_job_attributes_form(): 334 | from pyipptool.forms import get_job_attributes_form 335 | request = get_job_attributes_form.render( 336 | {'operation_attributes_tag': 337 | {'printer_uri': 338 | 'https://localhost:631/printers/DA-PRINTER', 339 | 'job_id': 2, 340 | 'requesting_user_name': 'susan', 341 | 'requested_attributes': 'job-uri'}}) 342 | assert 'NAME "Get Job Attributes"' in request 343 | assert 'OPERATION "Get-Job-Attributes"' in request 344 | assert 'ATTR uri printer-uri https://localhost:631/printers/DA-PRINTER'\ 345 | in request 346 | assert 'ATTR integer job-id 2' in request 347 | assert 'ATTR name requesting-user-name susan' in request 348 | assert 'ATTR keyword requested-attributes job-uri' in request 349 | 350 | 351 | def test_get_jobs_form(): 352 | from pyipptool.forms import get_jobs_form 353 | request = get_jobs_form.render( 354 | {'operation_attributes_tag': 355 | {'printer_uri': 'https://localhost:631/printers/p0', 356 | 'requesting_user_name': 'yoda', 357 | 'limit': 1, 358 | 'requested_attributes': 'job-uri', 359 | 'which_jobs': 'pending', 360 | 'my_jobs': True}}) 361 | assert 'NAME "Get Jobs"' in request 362 | assert 'OPERATION "Get-Jobs"' in request 363 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 364 | assert 'ATTR name requesting-user-name yoda' in request 365 | assert 'ATTR integer limit 1' in request 366 | assert 'ATTR keyword requested-attributes job-uri' in request 367 | assert 'ATTR keyword which-jobs pending' in request 368 | assert 'ATTR keyword requested-attributes job-uri' in request 369 | 370 | 371 | def test_get_printer_attributes_form(): 372 | from pyipptool.forms import get_printer_attributes_form 373 | request = get_printer_attributes_form.render( 374 | {'operation_attributes_tag': 375 | {'printer_uri': 376 | 'https://localhost:631/printers/p0', 377 | 'requesting_user_name': 'yoda', 378 | 'requested_attributes': ('printer-name', 'operations-supported')}}) 379 | assert 'NAME "Get Printer Attributes"' in request 380 | assert 'OPERATION "Get-Printer-Attributes"' in request 381 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 382 | assert 'ATTR name requesting-user-name yoda' in request 383 | assert 'ATTR keyword requested-attributes'\ 384 | ' printer-name,operations-supported' in request 385 | 386 | 387 | def test_get_subscriptions_form(): 388 | from pyipptool.forms import get_subscriptions_form 389 | request = get_subscriptions_form.render( 390 | {'operation_attributes_tag': 391 | {'printer_uri': 'https://localhost:631/printers/p0', 392 | 'requesting_user_name': 'yoda', 393 | 'notify_job_id': 3, 394 | 'limit': 1, 395 | 'requested_attributes': 'notify-recipient-uri', 396 | 'my_subscriptions': True}}) 397 | assert 'NAME "Get Subscriptions"' in request 398 | assert 'OPERATION "Get-Subscriptions"' in request 399 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 400 | assert 'ATTR name requesting-user-name yoda' in request 401 | assert 'ATTR integer notify-job-id 3' in request 402 | assert 'ATTR integer limit 1' in request 403 | assert 'ATTR keyword requested-attributes notify-recipient-uri' in request 404 | assert 'ATTR boolean my-subscriptions 1' in request 405 | 406 | 407 | def test_get_notifications_form_for_one_notification(): 408 | from pyipptool.forms import get_notifications_form 409 | request = get_notifications_form.render( 410 | {'operation_attributes_tag': 411 | {'printer_uri': 'https://localhost:631/printers/p0', 412 | 'requesting_user_name': 'yoda', 413 | 'notify_subscription_ids': 3, 414 | 'notify_sequence_numbers': 1, 415 | 'notify_wait': True}}) 416 | assert 'NAME "Get Notifications"' in request 417 | assert 'OPERATION "Get-Notifications"' in request 418 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 419 | assert 'ATTR name requesting-user-name yoda' in request 420 | assert 'ATTR integer notify-subscription-ids 3' in request 421 | assert 'ATTR integer notify-sequence-numbers 1' in request 422 | assert 'ATTR boolean notify-wait 1' in request 423 | 424 | 425 | def test_get_notifications_form_for_multiple_notifications(): 426 | from pyipptool.forms import get_notifications_form 427 | request = get_notifications_form.render( 428 | {'operation_attributes_tag': 429 | {'printer_uri': 'https://localhost:631/printers/p0', 430 | 'requesting_user_name': 'yoda', 431 | 'notify_subscription_ids': (3, 4, 5), 432 | 'notify_sequence_numbers': (2, 9, 29), 433 | 'notify_wait': True}}) 434 | assert 'NAME "Get Notifications"' in request 435 | assert 'OPERATION "Get-Notifications"' in request 436 | assert 'ATTR uri printer-uri https://localhost:631/printers/p0' in request 437 | assert 'ATTR name requesting-user-name yoda' in request 438 | assert 'ATTR integer notify-subscription-ids 3,4,5' in request 439 | assert 'ATTR integer notify-sequence-numbers 2,9,29' in request 440 | assert 'ATTR boolean notify-wait 1' in request 441 | 442 | 443 | def test_pause_printer_form(): 444 | from pyipptool.forms import pause_printer_form 445 | request = pause_printer_form.render( 446 | {'operation_attributes_tag': 447 | {'printer_uri': 'ipp://server:port/printers/name', 448 | 'requesting_user_name': 'yoda'}}) 449 | assert 'NAME "Pause Printer"' in request 450 | assert 'OPERATION "Pause-Printer"' in request 451 | assert 'ATTR uri printer-uri ipp://server:port/printers/name' in request 452 | assert 'ATTR name requesting-user-name yoda' in request 453 | 454 | 455 | def test_resume_printer_form(): 456 | from pyipptool.forms import resume_printer_form 457 | request = resume_printer_form.render( 458 | {'operation_attributes_tag': 459 | {'printer_uri': 'ipp://server:port/printers/name', 460 | 'requesting_user_name': 'yoda'}}) 461 | assert 'NAME "Resume Printer"' in request 462 | assert 'OPERATION "Resume-Printer"' in request 463 | assert 'ATTR uri printer-uri ipp://server:port/printers/name' in request 464 | assert 'ATTR name requesting-user-name yoda' in request 465 | 466 | 467 | def test_hold_new_jobs_form(): 468 | from pyipptool.forms import hold_new_jobs_form 469 | request = hold_new_jobs_form.render( 470 | {'operation_attributes_tag': 471 | {'printer_uri': 'ipp://server:port/printers/name', 472 | 'requesting_user_name': 'yoda', 473 | 'printer_message_from_operator': 'freeze jobs'}}) 474 | assert 'NAME "Hold New Jobs"' in request 475 | assert 'OPERATION "Hold-New-Jobs"' in request 476 | assert 'ATTR uri printer-uri ipp://server:port/printers/name' in request 477 | assert 'ATTR name requesting-user-name yoda' in request 478 | assert 'ATTR text printer-message-from-operator "freeze jobs"' in request 479 | 480 | 481 | def test_release_held_new_jobs_form(): 482 | from pyipptool.forms import release_held_new_jobs_form 483 | request = release_held_new_jobs_form.render( 484 | {'operation_attributes_tag': 485 | {'printer_uri': 'ipp://server:port/printers/name', 486 | 'requesting_user_name': 'yoda', 487 | 'printer_message_from_operator': 'melt jobs'}}) 488 | assert 'NAME "Release Held New Jobs"' in request 489 | assert 'OPERATION "Release-Held-New-Jobs"' in request 490 | assert 'ATTR uri printer-uri ipp://server:port/printers/name' in request 491 | assert 'ATTR name requesting-user-name yoda' in request 492 | assert 'ATTR text printer-message-from-operator "melt jobs"' in request 493 | 494 | 495 | def test_cancel_subscription_form(): 496 | from pyipptool.forms import cancel_subscription_form 497 | request = cancel_subscription_form.render( 498 | {'operation_attributes_tag': 499 | {'printer_uri': 'ipp://server:port/printers/name', 500 | 'requesting_user_name': 'yoda', 501 | 'notify_subscription_id': 5}}) 502 | assert 'NAME "Cancel Subscription"' in request 503 | assert 'OPERATION "Cancel-Subscription"' in request 504 | assert 'ATTR uri printer-uri ipp://server:port/printers/name' in request 505 | assert 'ATTR name requesting-user-name yoda' in request 506 | assert 'ATTR integer notify-subscription-id 5' in request, request 507 | 508 | 509 | def test_create_job_form(): 510 | """ 511 | http://www.cups.org/documentation.php/spec-ipp.html#CREATE_JOB 512 | """ 513 | from pyipptool.forms import create_job_form 514 | request = create_job_form.render( 515 | {'operation_attributes_tag': 516 | {'printer_uri': 'ipp://server:port/printers/name', 517 | 'job_name': 'foo', 518 | 'ipp_attribute_fidelity': True, 519 | 'job_k_octets': 1024, 520 | 'job_impressions': 2048, 521 | 'job_media_sheets': 2}, 522 | 'job_attributes_tag': 523 | {'job_priority': 1, 524 | 'job_hold_until': 'indefinite', 525 | 'job_sheets': 'standard', 526 | 'media': 'iso-a4-white', 527 | 'auth_info': 'michael', 528 | 'job_billing': 'no-idea', 529 | 'multiple_document_handling': 'single-document', 530 | 'copies': 2, 531 | 'finishings': 'punch', 532 | 'page_ranges': '1-6', 533 | 'sides': 'two-sided-short-edge', 534 | 'number_up': 4, 535 | 'orientation_requested': 'reverse-landscape', 536 | 'printer_resolution': '600dpi', 537 | 'print_quality': 5}}) 538 | assert 'NAME "Create Job"' in request 539 | assert 'OPERATION "Create-Job"' in request 540 | assert ('ATTR uri printer-uri ipp://server:port/printers/name' in 541 | request), request 542 | assert 'ATTR name job-name foo' in request 543 | assert 'ATTR boolean ipp-attribute-fidelity 1' in request, request 544 | assert 'ATTR integer job-k-octets 1024' in request 545 | assert 'ATTR integer job-impressions 2048' in request 546 | assert 'ATTR integer job-media-sheets 2' in request 547 | assert 'ATTR text auth-info "michael"' in request, request 548 | assert 'ATTR text job-billing "no-idea"' in request, request 549 | 550 | assert 'GROUP job-attributes-tag' in request 551 | assert 'ATTR integer job-priority 1' in request 552 | assert 'ATTR keyword job-hold-until indefinite' in request 553 | assert 'ATTR keyword job-sheets standard' in request 554 | assert 'ATTR keyword multiple-document-handling single-document' in request 555 | assert 'ATTR integer copies 2' in request 556 | assert 'ATTR enum finishings punch' in request 557 | assert 'ATTR rangeOfInteger page-ranges 1-6' in request 558 | assert 'ATTR keyword sides two-sided-short-edge' in request 559 | assert 'ATTR integer number-up 4' in request 560 | assert 'ATTR enum orientation-requested reverse-landscape' in request 561 | assert 'ATTR keyword media iso-a4-white' in request 562 | assert 'ATTR resolution printer-resolution 600dpi' in request 563 | assert 'ATTR enum print-quality 5' in request 564 | 565 | 566 | def test_print_job_form(): 567 | from pyipptool.forms import print_job_form 568 | request = print_job_form.render( 569 | {'operation_attributes_tag': 570 | {'printer_uri': 'ipp://server:port/printers/name', 571 | 'job_name': 'foo', 572 | 'ipp_attribute_fidelity': True, 573 | 'document_name': 'foo.txt', 574 | 'compression': 'gzip', 575 | 'document_format': 'text/plain', 576 | 'document_natural_language': 'en', 577 | 'job_k_octets': 1024, 578 | 'job_impressions': 2048, 579 | 'job_media_sheets': 2}, 580 | 'job_attributes_tag': 581 | {'job_priority': 1, 582 | 'job_hold_until': 'indefinite', 583 | 'job_sheets': 'standard', 584 | 'auth_info': 'michael', 585 | 'job_billing': 'no-idea', 586 | 'media': 'media-default', 587 | 'multiple_document_handling': 'single-document', 588 | 'copies': 2, 589 | 'finishings': 'punch', 590 | 'page_ranges': '1-6', 591 | 'sides': 'two-sided-short-edge', 592 | 'number_up': 4, 593 | 'orientation_requested': 'reverse-landscape', 594 | 'printer_resolution': '600dpi', 595 | 'print_quality': 5}, 596 | 'subscription_attributes_tag': 597 | {'notify_recipient_uri': 'rss://', 598 | 'notify_events': ['all']}, 599 | 'document_attributes_tag': 600 | {'file': '/path/to/file.txt'}}) 601 | 602 | assert 'NAME "Print Job"' in request 603 | assert 'OPERATION "Print-Job"' in request 604 | assert ('ATTR uri printer-uri ipp://server:port/printers/name' in 605 | request), request 606 | assert 'GROUP operation-attributes-tag' in request 607 | assert 'ATTR name job-name foo' in request 608 | assert 'ATTR boolean ipp-attribute-fidelity 1' in request, request 609 | assert 'ATTR name document-name foo.txt' in request 610 | assert 'ATTR keyword compression gzip' in request 611 | assert 'ATTR mimeMediaType document-format text/plain' in request 612 | assert 'ATTR naturalLanguage document-natural-language en' in request 613 | assert 'ATTR integer job-k-octets 1024' in request 614 | assert 'ATTR integer job-impressions 2048' in request 615 | assert 'ATTR integer job-media-sheets 2' in request 616 | 617 | assert 'GROUP job-attributes-tag' in request 618 | assert 'ATTR integer job-priority 1' in request 619 | assert 'ATTR keyword job-hold-until indefinite' in request 620 | assert 'ATTR keyword job-sheets standard' in request 621 | assert 'ATTR text auth-info "michael"' in request, request 622 | assert 'ATTR text job-billing "no-idea"' in request, request 623 | assert 'ATTR keyword job-sheets standard' in request, request 624 | assert 'ATTR keyword media media-default' in request 625 | assert 'ATTR keyword multiple-document-handling single-document' in request 626 | assert 'ATTR integer copies 2' in request 627 | assert 'ATTR enum finishings punch' in request 628 | assert 'ATTR rangeOfInteger page-ranges 1-6' in request 629 | assert 'ATTR keyword sides two-sided-short-edge' in request 630 | assert 'ATTR integer number-up 4' in request 631 | assert 'ATTR enum orientation-requested reverse-landscape' in request 632 | assert 'ATTR resolution printer-resolution 600dpi' in request 633 | assert 'ATTR enum print-quality 5' in request 634 | 635 | assert 'GROUP subscription-attributes-tag' in request 636 | assert 'ATTR uri notify-recipient-uri rss://' in request 637 | assert 'ATTR keyword notify-events all' in request 638 | 639 | assert 'GROUP document-attributes-tag' in request 640 | assert 'FILE /path/to/file.txt' in request 641 | 642 | assert (request.index('GROUP operation-attributes-tag') < 643 | request.index('GROUP job-attributes-tag') < 644 | request.index('GROUP subscription-attributes-tag') < 645 | request.index('GROUP document-attributes-tag') 646 | ) 647 | 648 | 649 | def test_send_document_form(): 650 | from pyipptool.forms import send_document_form 651 | 652 | request = send_document_form.render( 653 | {'operation_attributes_tag': 654 | {'job_uri': 'http://cups:631/jobs/2', 655 | 'requesting_user_name': 'sweet', 656 | 'document_name': 'python.pdf', 657 | 'compression': 'gzip', 658 | 'document_format': 'application/pdf', 659 | 'document_natural_language': 'en', 660 | 'last_document': True}, 661 | 'document_attributes_tag': 662 | {'file': '/path/to/a/file.pdf'}}) 663 | assert 'NAME "Send Document"' in request 664 | assert 'OPERATION "Send-Document"' in request 665 | 666 | assert 'GROUP operation-attributes-tag' in request 667 | assert 'ATTR uri job-uri http://cups:631/jobs/2' in request 668 | assert 'ATTR name requesting-user-name sweet' in request 669 | assert 'ATTR name document-name python.pdf' in request 670 | assert 'ATTR keyword compression gzip' in request 671 | assert 'ATTR mimeMediaType document-format application/pdf' in request 672 | assert 'ATTR naturalLanguage document-natural-language en' in request 673 | assert 'ATTR boolean last-document 1' in request, request 674 | 675 | assert 'GROUP document-attributes-tag' 676 | assert 'FILE /path/to/a/file.pdf' in request, request 677 | 678 | 679 | def test_range_of_integer_validator(): 680 | from pyipptool.schemas import range_of_integer_validator 681 | with pytest.raises(colander.Invalid): 682 | range_of_integer_validator(mock.MagicMock(), '12-98d') 683 | 684 | with pytest.raises(colander.Invalid): 685 | range_of_integer_validator(mock.MagicMock(), '-12') 686 | 687 | with pytest.raises(colander.Invalid): 688 | range_of_integer_validator(mock.MagicMock(), '10-9') 689 | 690 | range_of_integer_validator(mock.MagicMock(), '2-87') 691 | -------------------------------------------------------------------------------- /pyipptool/core.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import os 4 | import plistlib 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | import time 9 | import threading 10 | 11 | from future import standard_library 12 | from future.builtins import bytes, str 13 | from future.utils import PY3 14 | with standard_library.hooks(): 15 | import urllib.parse 16 | 17 | import colander 18 | 19 | from .forms import (cancel_job_form, 20 | release_job_form, 21 | create_job_form, 22 | create_job_subscription_form, 23 | create_printer_subscription_form, 24 | cups_add_modify_class_form, 25 | cups_add_modify_printer_form, 26 | cups_delete_printer_form, 27 | cups_delete_class_form, 28 | cups_get_classes_form, 29 | cups_get_devices_form, 30 | cups_get_ppd_form, 31 | cups_get_ppds_form, 32 | cups_get_printers_form, 33 | cups_move_job_form, 34 | cups_reject_jobs_form, 35 | get_job_attributes_form, 36 | get_jobs_form, 37 | get_printer_attributes_form, 38 | get_subscriptions_form, 39 | get_notifications_form, 40 | pause_printer_form, 41 | print_job_form, 42 | resume_printer_form, 43 | send_document_form, 44 | hold_new_jobs_form, 45 | release_held_new_jobs_form, 46 | cancel_subscription_form, 47 | ) 48 | 49 | try: 50 | from tornado.gen import coroutine, Return, Task 51 | from tornado.ioloop import TimeoutError 52 | except ImportError: 53 | def coroutine(f): 54 | return f 55 | 56 | class TimeoutError(Exception): 57 | pass 58 | 59 | class Return(Exception): 60 | def __init__(self, value): 61 | self.value = value 62 | 63 | 64 | def pyipptool_coroutine(method): 65 | """ 66 | Mark the method as a coroutine. 67 | If use with tornado the side effect of this decorator will be 68 | cancelled and the original_method will be wrapped by 69 | tornado.gen.coroutine . 70 | Otherwise the sync_coroutine_consumer wrapper 71 | will take care to consume the generator synchronously. 72 | """ 73 | method.ipptool_caller = True 74 | 75 | @functools.wraps(method) 76 | def sync_coroutine_consumer(*args, **kw): 77 | gen = method(*args, **kw) 78 | while True: 79 | value = next(gen) 80 | try: 81 | gen.send(value) 82 | except Return as returned: 83 | return returned.value 84 | except StopIteration: 85 | return value 86 | sync_coroutine_consumer.original_method = method 87 | return sync_coroutine_consumer 88 | 89 | 90 | def _get_filename_for_content(content): 91 | """ 92 | Return the name of a file based on type of content 93 | - already a file ? 94 | - does he have a name ? 95 | take its name 96 | - else 97 | copy to temp file and return its name 98 | - binary content ? 99 | copy to temp file and return its name 100 | 101 | if a temp file is created the caller is responsible to 102 | destroy the file. the flag delete is meant for it. 103 | """ 104 | file_ = None 105 | delete = False 106 | if content is colander.null: 107 | return content, delete 108 | if hasattr(getattr(content, 'file', None), 'read'): 109 | # tempfile 110 | file_ = content 111 | if hasattr(content, 'read'): 112 | # most likely a file like object 113 | file_ = content 114 | if file_ is not None: 115 | if file_.name: 116 | name = file_.name 117 | else: 118 | with tempfile.NamedTemporaryFile(delete=False, 119 | mode='rb') as tmp: 120 | delete = True 121 | shutil.copyfileobj(file_, tmp) 122 | name = tmp.name 123 | elif isinstance(content, (str, bytes)): 124 | with tempfile.NamedTemporaryFile(delete=False) as tmp: 125 | delete = True 126 | tmp.write(content) 127 | name = tmp.name 128 | else: 129 | raise NotImplementedError( 130 | 'Got unknow document\'s content type {}'.format( 131 | type(content))) 132 | 133 | return name, delete 134 | 135 | 136 | def pretty_printer(form): 137 | """ 138 | Remove blank lines 139 | """ 140 | return '\n'.join((line.strip() for line in form.splitlines() 141 | if line and not line.isspace())) 142 | 143 | 144 | class MetaAsyncShifter(type): 145 | """ 146 | Based on async flage defined on IPPToolWrapper 147 | methods will be decorated by tornado.gen.coroutine otherwise 148 | with a fake one. 149 | """ 150 | def __new__(cls, name, bases, attrs): 151 | klass = super(MetaAsyncShifter, cls).__new__(cls, name, bases, attrs) 152 | if attrs.get('async'): 153 | # ASYNC Wrapper 154 | for method_name in dir(bases[0]): 155 | method = getattr(bases[0], method_name) 156 | if getattr(method, 'ipptool_caller', False): 157 | # Patch Method with tornado.gen.coroutine 158 | setattr(klass, method_name, 159 | coroutine(method.original_method)) 160 | return klass 161 | 162 | 163 | class IPPToolWrapper(object): 164 | __metaclass__ = MetaAsyncShifter 165 | async = False 166 | 167 | def __init__(self, config): 168 | self.config = config 169 | 170 | @property 171 | def authenticated_uri(self): 172 | if 'login' in self.config and 'password' in self.config: 173 | parsed_url = urllib.parse.urlparse(self.config['cups_uri']) 174 | authenticated_netloc = '{}:{}@{}'.format(self.config['login'], 175 | self.config['password'], 176 | parsed_url.netloc) 177 | authenticated_uri = urllib.parse.ParseResult(parsed_url[0], 178 | authenticated_netloc, 179 | *parsed_url[2:]) 180 | return authenticated_uri.geturl() 181 | return self.config['cups_uri'] 182 | 183 | def timeout_handler(self, process, future): 184 | future.append(True) 185 | beginning = time.time() 186 | process.terminate() 187 | while process.poll() is None: 188 | if time.time() - beginning > self.config['graceful_shutdown_time']: 189 | try: 190 | process.kill() 191 | except OSError: 192 | pass 193 | break 194 | time.sleep(.1) 195 | 196 | def _call_ipptool(self, request): 197 | with tempfile.NamedTemporaryFile(delete=False) as temp_file: 198 | temp_file.write(bytes(request, encoding='utf-8')) 199 | process = subprocess.Popen([self.config['ipptool_path'], 200 | self.authenticated_uri, 201 | '-X', 202 | temp_file.name], 203 | stdin=subprocess.PIPE, 204 | stdout=subprocess.PIPE, 205 | stderr=subprocess.PIPE) 206 | future = [] 207 | timer = threading.Timer(self.config['timeout'], 208 | self.timeout_handler, (process, future)) 209 | timer.start() 210 | try: 211 | stdout, stderr = process.communicate() 212 | finally: 213 | os.unlink(temp_file.name) 214 | timer.cancel() 215 | if future: 216 | raise TimeoutError 217 | if PY3: 218 | result = plistlib.loads(stdout) 219 | else: 220 | result = plistlib.readPlistFromString(stdout) 221 | try: 222 | return result['Tests'][0] 223 | except (IndexError, KeyError): 224 | logger = logging.getLogger(__name__) 225 | logger.error('ipptool command failed: {} {}'.format(stdout, 226 | stderr)) 227 | raise 228 | 229 | @pyipptool_coroutine 230 | def release_job(self, 231 | printer_uri=colander.null, 232 | job_id=colander.null, 233 | job_uri=colander.null): 234 | kw = {'operation_attributes_tag': 235 | {'printer_uri': printer_uri, 236 | 'job_id': job_id, 237 | 'job_uri': job_uri}} 238 | request = pretty_printer(release_job_form.render(kw)) 239 | response = yield self._call_ipptool(request) 240 | raise Return(response) 241 | 242 | @pyipptool_coroutine 243 | def cancel_job(self, 244 | printer_uri=colander.null, 245 | job_id=colander.null, 246 | job_uri=colander.null, 247 | purge_job=colander.null): 248 | kw = {'operation_attributes_tag': 249 | {'printer_uri': printer_uri, 250 | 'job_id': job_id, 251 | 'job_uri': job_uri, 252 | 'purge_job': purge_job}} 253 | request = pretty_printer(cancel_job_form.render(kw)) 254 | response = yield self._call_ipptool(request) 255 | raise Return(response) 256 | 257 | @pyipptool_coroutine 258 | def create_job(self, 259 | printer_uri=None, 260 | job_name=colander.null, 261 | ipp_attribute_fidelity=colander.null, 262 | job_k_octets=colander.null, 263 | job_impressions=colander.null, 264 | job_media_sheets=colander.null, 265 | job_priority=colander.null, 266 | job_hold_until=colander.null, 267 | multiple_document_handling=colander.null, 268 | copies=colander.null, 269 | finishings=colander.null, 270 | page_ranges=colander.null, 271 | sides=colander.null, 272 | number_up=colander.null, 273 | orientation_requested=colander.null, 274 | printer_resolution=colander.null, 275 | print_quality=colander.null, 276 | auth_info=colander.null, 277 | job_billing=colander.null, 278 | job_sheets=colander.null, 279 | media=colander.null): 280 | kw = {'operation_attributes_tag': 281 | {'printer_uri': printer_uri, 282 | 'job_name': job_name, 283 | 'ipp_attribute_fidelity': ipp_attribute_fidelity, 284 | 'job_k_octets': job_k_octets, 285 | 'job_impressions': job_impressions, 286 | 'job_media_sheets': job_media_sheets}, 287 | 'job_attributes_tag': 288 | {'job_priority': job_priority, 289 | 'job_hold_until': job_hold_until, 290 | 'job_sheets': job_sheets, 291 | 'multiple_document_handling': multiple_document_handling, 292 | 'copies': copies, 293 | 'finishings': finishings, 294 | 'page_ranges': page_ranges, 295 | 'sides': sides, 296 | 'number_up': number_up, 297 | 'orientation_requested': orientation_requested, 298 | 'media': media, 299 | 'printer_resolution': printer_resolution, 300 | 'print_quality': print_quality, 301 | 'auth_info': auth_info, 302 | 'job_billing': job_billing, 303 | 'job_sheets': job_sheets, 304 | 'media': media}} 305 | request = pretty_printer(create_job_form.render(kw)) 306 | response = yield self._call_ipptool(request) 307 | raise Return(response) 308 | 309 | @pyipptool_coroutine 310 | def print_job(self, 311 | printer_uri=None, 312 | requesting_user_name=colander.null, 313 | job_name=colander.null, 314 | ipp_attribute_fidelity=colander.null, 315 | document_name=colander.null, 316 | compression=colander.null, 317 | document_format=colander.null, 318 | document_natural_language=colander.null, 319 | job_k_octets=colander.null, 320 | job_impressions=colander.null, 321 | job_media_sheets=colander.null, 322 | job_priority=colander.null, 323 | job_hold_until=colander.null, 324 | multiple_document_handling=colander.null, 325 | copies=colander.null, 326 | finishings=colander.null, 327 | page_ranges=colander.null, 328 | sides=colander.null, 329 | number_up=colander.null, 330 | orientation_requested=colander.null, 331 | printer_resolution=colander.null, 332 | print_quality=colander.null, 333 | ezeep_job_uuid=colander.null, 334 | notify_recipient_uri=colander.null, 335 | notify_events=colander.null, 336 | notify_time_interval=colander.null, 337 | auth_info=colander.null, 338 | job_billing=colander.null, 339 | job_sheets=colander.null, 340 | media=colander.null, 341 | document_content=None, 342 | ): 343 | filename, delete = _get_filename_for_content(document_content) 344 | kw = {'operation_attributes_tag': 345 | {'printer_uri': printer_uri, 346 | 'requesting_user_name': requesting_user_name, 347 | 'job_name': job_name, 348 | 'ipp_attribute_fidelity': ipp_attribute_fidelity, 349 | 'document_name': document_name, 350 | 'compression': compression, 351 | 'document_format': document_format, 352 | 'document_natural_language': document_natural_language, 353 | 'job_k_octets': job_k_octets, 354 | 'job_impressions': job_impressions, 355 | 'job_media_sheets': job_media_sheets}, 356 | 'job_attributes_tag': 357 | {'job_priority': job_priority, 358 | 'job_hold_until': job_hold_until, 359 | 'job_sheets': job_sheets, 360 | 'auth_info': auth_info, 361 | 'job_billing': job_billing, 362 | 'multiple_document_handling': multiple_document_handling, 363 | 'copies': copies, 364 | 'finishings': finishings, 365 | 'page_ranges': page_ranges, 366 | 'sides': sides, 367 | 'number_up': number_up, 368 | 'orientation_requested': orientation_requested, 369 | 'media': media, 370 | 'printer_resolution': printer_resolution, 371 | 'print_quality': print_quality, 372 | 'ezeep_job_uuid': ezeep_job_uuid, 373 | }, 374 | 'subscription_attributes_tag': 375 | {'notify_recipient_uri': notify_recipient_uri, 376 | 'notify_events': notify_events, 377 | 'notify_time_interval': notify_time_interval}, 378 | 'document_attributes_tag': 379 | {'file': filename}} 380 | request = pretty_printer(print_job_form.render(kw)) 381 | try: 382 | response = yield self._call_ipptool(request) 383 | raise Return(response) 384 | finally: 385 | if delete: 386 | os.unlink(filename) 387 | 388 | @pyipptool_coroutine 389 | def create_job_subscription(self, 390 | requesting_user_name=None, 391 | printer_uri=colander.null, 392 | job_id=colander.null, 393 | job_uri=colander.null, 394 | notify_job_id=colander.null, 395 | notify_recipient_uri=colander.null, 396 | notify_pull_method=colander.null, 397 | notify_events=colander.null, 398 | notify_attributes=colander.null, 399 | notify_charset=colander.null, 400 | notify_natural_language=colander.null, 401 | notify_time_interval=colander.null): 402 | """ 403 | Create a per-job subscription object. 404 | 405 | 406 | Create-Job-Subscriptions 407 | https://tools.ietf.org/html/rfc3995#section-11.1.1 408 | 409 | https://www.cups.org/str.php?L4389 410 | """ 411 | kw = {'operation_attributes_tag': 412 | {'printer_uri': printer_uri, 413 | 'requesting_user_name': requesting_user_name, 414 | 'job_id': job_id, 415 | 'job_uri': job_uri}, 416 | 'subscription_attributes_tag': 417 | {'notify_job_id': notify_job_id, 418 | 'notify_recipient_uri': notify_recipient_uri, 419 | 'notify_pull_method': notify_pull_method, 420 | 'notify_events': notify_events, 421 | 'notify_attributes': notify_attributes, 422 | 'notify_charset': notify_charset, 423 | 'notify_natural_language': notify_natural_language, 424 | 'notify_time_interval': notify_time_interval}} 425 | request = pretty_printer(create_job_subscription_form.render(kw)) 426 | response = yield self._call_ipptool(request) 427 | raise Return(response) 428 | 429 | @pyipptool_coroutine 430 | def create_printer_subscription( 431 | self, 432 | printer_uri=None, 433 | requesting_user_name=None, 434 | notify_recipient_uri=colander.null, 435 | notify_pull_method=colander.null, 436 | notify_events=colander.null, 437 | notify_attributes=colander.null, 438 | notify_charset=colander.null, 439 | notify_natural_language=colander.null, 440 | notify_lease_duration=colander.null, 441 | notify_time_interval=colander.null): 442 | """ 443 | Create a new subscription and return its id 444 | """ 445 | kw = {'operation_attributes_tag': 446 | {'printer_uri': printer_uri, 447 | 'requesting_user_name': requesting_user_name}, 448 | 'subscription_attributes_tag': 449 | {'notify_recipient_uri': notify_recipient_uri, 450 | 'notify_pull_method': notify_pull_method, 451 | 'notify_events': notify_events, 452 | 'notify_attributes': notify_attributes, 453 | 'notify_charset': notify_charset, 454 | 'notify_natural_language': notify_natural_language, 455 | 'notify_lease_duration': notify_lease_duration, 456 | 'notify_time_interval': notify_time_interval}} 457 | request = pretty_printer(create_printer_subscription_form.render(kw)) 458 | response = yield self._call_ipptool(request) 459 | raise Return(response) 460 | 461 | @pyipptool_coroutine 462 | def cups_add_modify_printer(self, 463 | printer_uri=None, 464 | auth_info_required=colander.null, 465 | job_sheets_default=colander.null, 466 | device_uri=colander.null, 467 | port_monitor=colander.null, 468 | ppd_name=colander.null, 469 | printer_is_accepting_jobs=colander.null, 470 | printer_info=colander.null, 471 | printer_location=colander.null, 472 | printer_more_info=colander.null, 473 | printer_op_policy=colander.null, 474 | printer_state=colander.null, 475 | printer_state_message=colander.null, 476 | requesting_user_name_allowed=colander.null, 477 | requesting_user_name_denied=colander.null, 478 | printer_is_shared=colander.null, 479 | ppd_content=colander.null, 480 | ): 481 | filename, delete = _get_filename_for_content(ppd_content) 482 | kw = {'operation_attributes_tag': 483 | {'printer_uri': printer_uri}, 484 | 'printer_attributes_tag': 485 | {'auth_info_required': auth_info_required, 486 | 'job_sheets_default': job_sheets_default, 487 | 'device_uri': device_uri, 488 | 'port_monitor': port_monitor, 489 | 'ppd_name': ppd_name, 490 | 'printer_is_accepting_jobs': printer_is_accepting_jobs, 491 | 'printer_info': printer_info, 492 | 'printer_location': printer_location, 493 | 'printer_more_info': printer_more_info, 494 | 'printer_op_policy': printer_op_policy, 495 | 'printer_state': printer_state, 496 | 'printer_state_message': printer_state_message, 497 | 'requesting_user_name_allowed ': requesting_user_name_allowed, 498 | 'requesting_user_name_denied': requesting_user_name_denied, 499 | 'printer_is_shared': printer_is_shared, 500 | 'file': filename}} 501 | 502 | request = pretty_printer(cups_add_modify_printer_form.render(kw)) 503 | try: 504 | response = yield self._call_ipptool(request) 505 | raise Return(response) 506 | finally: 507 | if delete: 508 | os.unlink(filename) 509 | 510 | @pyipptool_coroutine 511 | def cups_add_modify_class(self, 512 | printer_uri=None, 513 | auth_info_required=colander.null, 514 | member_uris=colander.null, 515 | printer_is_accepting_jobs=colander.null, 516 | printer_info=colander.null, 517 | printer_location=colander.null, 518 | printer_more_info=colander.null, 519 | printer_op_policy=colander.null, 520 | printer_state=colander.null, 521 | printer_state_message=colander.null, 522 | requesting_user_name_allowed=colander.null, 523 | requesting_user_name_denied=colander.null, 524 | printer_is_shared=colander.null): 525 | kw = {'operation_attributes_tag': 526 | {'printer_uri': printer_uri}, 527 | 'printer_attributes_tag': 528 | {'auth_info_required': auth_info_required, 529 | 'member_uris': member_uris, 530 | 'printer_is_accepting_jobs': printer_is_accepting_jobs, 531 | 'printer_info': printer_info, 532 | 'printer_location': printer_location, 533 | 'printer_more_info': printer_more_info, 534 | 'printer_op_policy': printer_op_policy, 535 | 'printer_state': printer_state, 536 | 'printer_state_message': printer_state_message, 537 | 'requesting_user_name_allowed ': requesting_user_name_allowed, 538 | 'requesting_user_name_denied': requesting_user_name_denied, 539 | 'printer_is_shared': printer_is_shared}} 540 | 541 | request = pretty_printer(cups_add_modify_class_form.render(kw)) 542 | response = yield self._call_ipptool(request) 543 | raise Return(response) 544 | 545 | @pyipptool_coroutine 546 | def cups_delete_printer(self, printer_uri=None): 547 | kw = {'operation_attributes_tag': {'printer_uri': printer_uri}} 548 | request = pretty_printer(cups_delete_printer_form.render(kw)) 549 | response = yield self._call_ipptool(request) 550 | raise Return(response) 551 | 552 | @pyipptool_coroutine 553 | def cups_delete_class(self, printer_uri=None): 554 | kw = {'operation_attributes_tag': {'printer_uri': printer_uri}} 555 | request = pretty_printer(cups_delete_class_form.render(kw)) 556 | response = yield self._call_ipptool(request) 557 | raise Return(response) 558 | 559 | @pyipptool_coroutine 560 | def cups_get_classes(self, 561 | first_printer_name=colander.null, 562 | limit=colander.null, 563 | printer_location=colander.null, 564 | printer_type=colander.null, 565 | printer_type_mask=colander.null, 566 | requested_attributes=colander.null, 567 | requested_user_name=colander.null): 568 | kw = {'operation_attributes_tag': 569 | {'first_printer_name': first_printer_name, 570 | 'limit': limit, 571 | 'printer_location': printer_location, 572 | 'printer_type': printer_type, 573 | 'printer_type_mask': printer_type_mask, 574 | 'requested_attributes': requested_attributes, 575 | 'requested_user_name': requested_user_name}} 576 | request = pretty_printer(cups_get_classes_form.render(kw)) 577 | response = yield self._call_ipptool(request) 578 | raise Return(response) 579 | 580 | @pyipptool_coroutine 581 | def cups_get_devices(self, 582 | device_class=colander.null, 583 | exclude_schemes=colander.null, 584 | include_schemes=colander.null, 585 | limit=colander.null, 586 | requested_attributes=colander.null, 587 | timeout=colander.null): 588 | kw = {'operation_attributes_tag': 589 | {'device_class': device_class, 590 | 'exclude_schemes': exclude_schemes, 591 | 'include-schemes': include_schemes, 592 | 'limit': limit, 593 | 'requested_attributes': requested_attributes, 594 | 'timeout': timeout}} 595 | request = pretty_printer(cups_get_devices_form.render(kw)) 596 | response = yield self._call_ipptool(request) 597 | raise Return(response) 598 | 599 | @pyipptool_coroutine 600 | def cups_get_ppd(self, printer_uri=colander.null, ppd_name=colander.null): 601 | kw = {'operation_attributes_tag': 602 | {'printer_uri': printer_uri, 603 | 'ppd_name': ppd_name, 604 | }} 605 | request = pretty_printer(cups_get_ppd_form.render(kw)) 606 | response = yield self._call_ipptool(request) 607 | raise Return(response) 608 | 609 | @pyipptool_coroutine 610 | def cups_get_ppds(self, 611 | exclude_schemes=colander.null, 612 | include_schemes=colander.null, 613 | limit=colander.null, 614 | ppd_make=colander.null, 615 | ppd_make_and_model=colander.null, 616 | ppd_model_number=colander.null, 617 | ppd_natural_language=colander.null, 618 | ppd_product=colander.null, 619 | ppd_psversion=colander.null, 620 | ppd_type=colander.null, 621 | requested_attributes=colander.null): 622 | kw = {'operation_attributes_tag': 623 | {'exclude_schemes': exclude_schemes, 624 | 'include_schemes': include_schemes, 625 | 'limit': limit, 626 | 'ppd_make': ppd_make, 627 | 'ppd_make_and_model': ppd_make_and_model, 628 | 'ppd_model_number': ppd_model_number, 629 | 'ppd_natural_language': ppd_natural_language, 630 | 'ppd_product': ppd_product, 631 | 'ppd_psversion': ppd_psversion, 632 | 'ppd_type': ppd_type, 633 | 'requested_attributes': requested_attributes 634 | }} 635 | request = pretty_printer(cups_get_ppds_form.render(kw)) 636 | response = yield self._call_ipptool(request) 637 | raise Return(response) 638 | 639 | @pyipptool_coroutine 640 | def cups_get_printers(self, 641 | first_printer_name=colander.null, 642 | limit=colander.null, 643 | printer_location=colander.null, 644 | printer_type=colander.null, 645 | printer_type_mask=colander.null, 646 | requested_attributes=colander.null, 647 | requested_user_name=colander.null): 648 | kw = {'operation_attributes_tag': 649 | {'first_printer_name': first_printer_name, 650 | 'limit': limit, 651 | 'printer_location': printer_location, 652 | 'printer_type': printer_type, 653 | 'printer_type_mask': printer_type_mask, 654 | 'requested_attributes': requested_attributes, 655 | 'requested_user_name': requested_user_name}} 656 | request = pretty_printer(cups_get_printers_form.render(kw)) 657 | response = yield self._call_ipptool(request) 658 | raise Return(response) 659 | 660 | @pyipptool_coroutine 661 | def cups_move_job(self, 662 | printer_uri=colander.null, 663 | job_id=colander.null, 664 | job_uri=colander.null, 665 | job_printer_uri=None, 666 | printer_state_message=colander.null): 667 | kw = {'operation_attributes_tag': 668 | {'printer_uri': printer_uri, 669 | 'job_id': job_id, 670 | 'job_uri': job_uri}, 671 | 'job_attributes_tag': 672 | {'job_printer_uri': job_printer_uri, 673 | 'printer_state_message': printer_state_message}} 674 | request = pretty_printer(cups_move_job_form.render(kw)) 675 | response = yield self._call_ipptool(request) 676 | raise Return(response) 677 | 678 | @pyipptool_coroutine 679 | def cups_reject_jobs(self, 680 | printer_uri=None, 681 | requesting_user_name=None, 682 | printer_state_message=colander.null): 683 | kw = {'operation_attributes_tag': 684 | {'printer_uri': printer_uri, 685 | 'requesting_user_name': requesting_user_name}, 686 | 'printer_attributes_tag': 687 | {'printer_state_message': printer_state_message}} 688 | request = pretty_printer(cups_reject_jobs_form.render(kw)) 689 | response = yield self._call_ipptool(request) 690 | raise Return(response) 691 | 692 | @pyipptool_coroutine 693 | def get_job_attributes(self, 694 | printer_uri=colander.null, 695 | job_id=colander.null, 696 | job_uri=colander.null, 697 | requesting_user_name=colander.null, 698 | requested_attributes=colander.null): 699 | kw = {'operation_attributes_tag': 700 | {'printer_uri': printer_uri, 701 | 'job_id': job_id, 702 | 'job_uri': job_uri, 703 | 'requesting_user_name': requesting_user_name, 704 | 'requested_attributes': requested_attributes}} 705 | request = pretty_printer(get_job_attributes_form.render(kw)) 706 | response = yield self._call_ipptool(request) 707 | raise Return(response) 708 | 709 | @pyipptool_coroutine 710 | def get_jobs(self, 711 | printer_uri=None, 712 | requesting_user_name=colander.null, 713 | limit=colander.null, 714 | requested_attributes=colander.null, 715 | which_jobs=colander.null, 716 | my_jobs=colander.null): 717 | kw = {'operation_attributes_tag': 718 | {'printer_uri': printer_uri, 719 | 'requesting_user_name': requesting_user_name, 720 | 'limit': limit, 721 | 'requested_attributes': requested_attributes, 722 | 'which_jobs': which_jobs, 723 | 'my_jobs': my_jobs}} 724 | request = pretty_printer(get_jobs_form.render(kw)) 725 | response = yield self._call_ipptool(request) 726 | raise Return(response) 727 | 728 | @pyipptool_coroutine 729 | def get_printer_attributes(self, 730 | printer_uri=None, 731 | requesting_user_name=colander.null, 732 | requested_attributes=colander.null): 733 | kw = {'operation_attributes_tag': 734 | {'printer_uri': printer_uri, 735 | 'requesting_user_name': requesting_user_name, 736 | 'requested_attributes': requested_attributes}} 737 | request = pretty_printer(get_printer_attributes_form.render(kw)) 738 | response = yield self._call_ipptool(request) 739 | raise Return(response) 740 | 741 | @pyipptool_coroutine 742 | def get_subscriptions(self, 743 | printer_uri=None, 744 | requesting_user_name=colander.null, 745 | notify_job_id=colander.null, 746 | limit=colander.null, 747 | requested_attributes=colander.null, 748 | my_subscriptions=colander.null): 749 | kw = {'operation_attributes_tag': 750 | {'printer_uri': printer_uri, 751 | 'requesting_user_name': requesting_user_name, 752 | 'notify_job_id': notify_job_id, 753 | 'limit': limit, 754 | 'requested_attributes': requested_attributes, 755 | 'my_subscriptions': my_subscriptions}} 756 | request = pretty_printer(get_subscriptions_form.render(kw)) 757 | response = yield self._call_ipptool(request) 758 | raise Return(response) 759 | 760 | @pyipptool_coroutine 761 | def get_notifications(self, 762 | printer_uri=None, 763 | notify_subscription_ids=None, 764 | requesting_user_name=colander.null, 765 | notify_sequence_numbers=colander.null, 766 | notify_wait=colander.null): 767 | kw = {'operation_attributes_tag': 768 | {'printer_uri': printer_uri, 769 | 'requesting_user_name': requesting_user_name, 770 | 'notify_subscription_ids': notify_subscription_ids, 771 | 'notify_sequence_numbers': notify_sequence_numbers, 772 | 'notify_wait': notify_wait}} 773 | request = pretty_printer(get_notifications_form.render(kw)) 774 | response = yield self._call_ipptool(request) 775 | raise Return(response) 776 | 777 | @pyipptool_coroutine 778 | def cancel_subscription(self, 779 | printer_uri=None, 780 | requesting_user_name=colander.null, 781 | notify_subscription_id=None): 782 | kw = {'operation_attributes_tag': 783 | {'printer_uri': printer_uri, 784 | 'requesting_user_name': requesting_user_name, 785 | 'notify_subscription_id': notify_subscription_id}} 786 | request = pretty_printer(cancel_subscription_form.render(kw)) 787 | response = yield self._call_ipptool(request) 788 | raise Return(response) 789 | 790 | @pyipptool_coroutine 791 | def _pause_or_resume_printer(self, form, printer_uri=None, 792 | requesting_user_name=colander.null): 793 | kw = {'operation_attributes_tag': 794 | {'printer_uri': printer_uri, 795 | 'requesting_user_name': requesting_user_name}} 796 | request = pretty_printer(form.render(kw)) 797 | response = yield self._call_ipptool(request) 798 | raise Return(response) 799 | 800 | def pause_printer(self, *args, **kw): 801 | return self._pause_or_resume_printer(pause_printer_form, *args, **kw) 802 | 803 | def resume_printer(self, *args, **kw): 804 | return self._pause_or_resume_printer(resume_printer_form, *args, **kw) 805 | 806 | @pyipptool_coroutine 807 | def _hold_or_release_new_jobs(self, form, printer_uri=None, 808 | requesting_user_name=colander.null, 809 | printer_message_from_operator=colander.null): 810 | kw = {'operation_attributes_tag': 811 | {'printer_uri': printer_uri, 812 | 'requesting_user_name': requesting_user_name, 813 | 'printer_message_from_operator': printer_message_from_operator 814 | }} 815 | request = pretty_printer(form.render(kw)) 816 | response = yield self._call_ipptool(request) 817 | raise Return(response) 818 | 819 | def hold_new_jobs(self, *args, **kw): 820 | return self._hold_or_release_new_jobs(hold_new_jobs_form, *args, **kw) 821 | 822 | def release_held_new_jobs(self, *args, **kw): 823 | return self._hold_or_release_new_jobs(release_held_new_jobs_form, 824 | *args, **kw) 825 | 826 | @pyipptool_coroutine 827 | def send_document(self, 828 | job_uri=colander.null, 829 | printer_uri=colander.null, 830 | job_id=colander.null, 831 | requesting_user_name=None, 832 | document_name=colander.null, 833 | compression=colander.null, 834 | document_format='application/pdf', 835 | document_natural_language=colander.null, 836 | last_document=True, 837 | document_content=None, 838 | ): 839 | """ 840 | :param document_content: Binary Content or Named File 841 | """ 842 | delete = False 843 | filename, delete = _get_filename_for_content(document_content) 844 | kw = {'operation_attributes_tag': 845 | {'job_uri': job_uri, 846 | 'printer_uri': printer_uri, 847 | 'job_id': job_id, 848 | 'requesting_user_name': requesting_user_name, 849 | 'document_name': document_name, 850 | 'compression': compression, 851 | 'document_format': document_format, 852 | 'document_natural_language': document_natural_language, 853 | 'last_document': last_document}, 854 | 'document_attributes_tag': {'file': filename}} 855 | request = pretty_printer(send_document_form.render(kw)) 856 | try: 857 | response = yield self._call_ipptool(request) 858 | raise Return(response) 859 | finally: 860 | if delete: 861 | os.unlink(filename) 862 | 863 | 864 | class AsyncIPPToolWrapper(IPPToolWrapper): 865 | async = True 866 | 867 | def __init__(self, config, io_loop): 868 | self.config = config 869 | self.io_loop = io_loop 870 | 871 | @coroutine 872 | def _call_ipptool(self, request): 873 | with tempfile.NamedTemporaryFile(delete=False) as temp_file: 874 | temp_file.write(bytes(request, encoding='utf-8')) 875 | from tornado.process import Subprocess 876 | process = Subprocess([self.config['ipptool_path'], 877 | self.authenticated_uri, '-X', 878 | temp_file.name], 879 | stdin=subprocess.PIPE, 880 | stdout=Subprocess.STREAM, 881 | stderr=Subprocess.STREAM, 882 | io_loop=self.io_loop) 883 | future = [] 884 | self.io_loop.add_timeout(self.io_loop.time() + self.config['timeout'], 885 | functools.partial(self.timeout_handler, 886 | process.proc, future)) 887 | try: 888 | stdout, stderr = yield [Task(process.stdout.read_until_close), 889 | Task(process.stderr.read_until_close)] 890 | if future: 891 | raise TimeoutError 892 | finally: 893 | os.unlink(temp_file.name) 894 | 895 | result = plistlib.readPlistFromString(stdout) 896 | try: 897 | raise Return(result['Tests'][0]) 898 | except (IndexError, KeyError): 899 | logger = logging.getLogger(__name__) 900 | logger.error('ipptool command failed: {} {}'.format(stdout, 901 | stderr)) 902 | raise 903 | --------------------------------------------------------------------------------