Make AssuanClient.make_request() responses optional.
[pyassuan.git] / pyassuan / common.py
1 # Copyright
2
3 """Items common to both the client and server
4 """
5
6 import re as _re
7
8 from . import error as _error
9
10
11 _ENCODE_REGEXP = _re.compile(
12     '(' + '|'.join(['%', '\r', '\n']) + ')')
13 _DECODE_REGEXP = _re.compile('(%[0-9A-F]{2})')
14 _REQUEST_REGEXP = _re.compile('^(\w+)( *)(.*)\Z')
15
16
17 def encode(string):
18     r"""
19
20     >>> encode('It grew by 5%!\n')
21     'It grew by 5%25!%0A'
22     """   
23     return _ENCODE_REGEXP.sub(
24         lambda x : to_hex(x.group()), string)
25
26 def decode(string):
27     r"""
28
29     >>> decode('%22Look out!%22%0AWhere%3F')
30     '"Look out!"\nWhere?'
31     """
32     return _DECODE_REGEXP.sub(
33         lambda x : from_hex(x.group()), string)
34
35 def from_hex(code):
36     r"""
37
38     >>> from_hex('%22')
39     '"'
40     >>> from_hex('%0A')
41     '\n'
42     """
43     return chr(int(code[1:], 16))
44
45 def to_hex(char):
46     r"""
47
48     >>> to_hex('"')
49     '%22'
50     >>> to_hex('\n')
51     '%0A'
52     """
53     return '%{:02X}'.format(ord(char))
54
55
56 class Request (object):
57     """A client request
58
59     http://www.gnupg.org/documentation/manuals/assuan/Client-requests.html
60
61     >>> r = Request(command='BYE')
62     >>> str(r)
63     'BYE'
64     >>> r = Request(command='OPTION', parameters='testing at 5%')
65     >>> str(r)
66     'OPTION testing at 5%25'
67     >>> r.from_string('BYE')
68     >>> r.command
69     'BYE'
70     >>> print(r.parameters)
71     None
72     >>> r.from_string('OPTION testing at 5%25')
73     >>> r.command
74     'OPTION'
75     >>> print(r.parameters)
76     testing at 5%
77     >>> r.from_string(' invalid')
78     Traceback (most recent call last):
79       ...
80     pyassuan.error.AssuanError: 170 Invalid request
81     >>> r.from_string('in-valid')
82     Traceback (most recent call last):
83       ...
84     pyassuan.error.AssuanError: 170 Invalid request
85     """
86     def __init__(self, command=None, parameters=None):
87         self.command = command
88         self.parameters = parameters
89
90     def __str__(self):
91         if self.parameters:
92             return '{} {}'.format(self.command, encode(self.parameters))
93         return self.command
94
95     def from_string(self, string):
96         if len(string) > 1000:  # TODO: byte-vs-str and newlines?
97             raise _error.AssuanError(message='Line too long')
98         match = _REQUEST_REGEXP.match(string)
99         if not match:
100             raise _error.AssuanError(message='Invalid request')
101         self.command = match.group(1)
102         if match.group(3):
103             if match.group(2):
104                 self.parameters = decode(match.group(3))
105             else:
106                 raise _error.AssuanError(message='Invalid request')
107         else:
108             self.parameters = None
109
110
111 class Response (object):
112     """A server response
113
114     http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
115
116     >>> r = Response(type='OK')
117     >>> str(r)
118     'OK'
119     >>> r = Response(type='ERR', parameters='1 General error')
120     >>> str(r)
121     'ERR 1 General error'
122     >>> r.from_string('OK')
123     >>> r.type
124     'OK'
125     >>> print(r.parameters)
126     None
127     >>> r.from_string('ERR 1 General error')
128     >>> r.type
129     'ERR'
130     >>> print(r.parameters)
131     1 General error
132     >>> r.from_string(' invalid')
133     Traceback (most recent call last):
134       ...
135     pyassuan.error.AssuanError: 76 Invalid response
136     >>> r.from_string('in-valid')
137     Traceback (most recent call last):
138       ...
139     pyassuan.error.AssuanError: 76 Invalid response
140     """
141     types = {
142         'O': 'OK',
143         'E': 'ERR',
144         'S': 'S',
145         '#': '#',
146         'D': 'D',
147         'I': 'INQUIRE',
148         }
149
150     def __init__(self, type=None, parameters=None):
151         self.type = type
152         self.parameters = parameters
153
154     def __str__(self):
155         if self.parameters:
156             return '{} {}'.format(self.type, encode(self.parameters))
157         return self.type
158
159     def from_string(self, string):
160         if len(string) > 1000:  # TODO: byte-vs-str and newlines?
161             raise _error.AssuanError(message='Line too long')
162         try:
163             type = self.types[string[0]]
164         except KeyError:
165             raise _error.AssuanError(message='Invalid response')
166         self.type = type
167         if type == 'D':  # data
168             self.parameters = decode(string[2:])
169         elif type == '#':  # comment
170             self.parameters = decode(string[2:])
171         else:
172             match = _REQUEST_REGEXP.match(string)
173             if not match:
174                 raise _error.AssuanError(message='Invalid request')
175             if match.group(3):
176                 if match.group(2):
177                     self.parameters = decode(match.group(3))
178                 else:
179                     raise _error.AssuanError(message='Invalid request')
180             else:
181                 self.parameters = None
182
183
184 def error_response(error):
185     """
186
187     >>> from pyassuan.error import AssuanError
188     >>> error = AssuanError(1)
189     >>> response = error_response(error)
190     >>> print(response)
191     ERR 1 General error
192     """
193     return Response(type='ERR', parameters=str(error))