aboutsummaryrefslogtreecommitdiff
path: root/dodai/config
diff options
context:
space:
mode:
Diffstat (limited to 'dodai/config')
-rw-r--r--dodai/config/__init__.py64
-rw-r--r--dodai/config/db/__init__.py179
-rw-r--r--dodai/config/db/sa.py64
-rw-r--r--dodai/config/file.py116
-rw-r--r--dodai/config/log.py150
-rw-r--r--dodai/config/option.py61
-rw-r--r--dodai/config/sections.py172
7 files changed, 806 insertions, 0 deletions
diff --git a/dodai/config/__init__.py b/dodai/config/__init__.py
new file mode 100644
index 0000000..aac9be7
--- /dev/null
+++ b/dodai/config/__init__.py
@@ -0,0 +1,64 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19
20class Config(object):
21
22 def __call__(self):
23 obj = ConfigResults()
24 if hasattr(self, 'vars'):
25 for key, val in self.vars.items():
26 setattr(obj, key, val)
27 return obj
28
29 def set(self, key, val):
30 if not hasattr(self, 'vars'):
31 self.vars = {}
32 self.vars[key] = val
33
34 def options(self):
35 if not hasattr(self, '_options'):
36 from dodai.config.option import ConfigOption
37 self._options = ConfigOption()
38 return self._options
39
40 def files(self):
41 if not hasattr(self, '_files'):
42 from dodai.config.file import ConfigFile
43 self._files = ConfigFile()
44 return self._files
45
46 def logs(self):
47 if not hasattr(self, '_logs'):
48 from dodai.config.log import ConfigLog
49 self._logs = ConfigLog()
50 return self._logs
51
52 def dbs(self):
53 if not hasattr(self, '_dbs'):
54 from dodai.config.db import ConfigDb
55 self._dbs = ConfigDb()
56 return self._dbs
57
58
59
60
61
62
63class ConfigResults(object):
64 pass
diff --git a/dodai/config/db/__init__.py b/dodai/config/db/__init__.py
new file mode 100644
index 0000000..fa510ac
--- /dev/null
+++ b/dodai/config/db/__init__.py
@@ -0,0 +1,179 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18class ConfigDb(object):
19
20 def __init__(self):
21 self.connections = {}
22 self._handlers = {}
23 from dodai.config.db.sa import Sa
24 self.register_handler('sa', Sa)
25
26 def register_handler(self, name, obj):
27 self._handlers[name] = [obj, None]
28
29 def add_config(self, config_parser=None):
30 if config_parser:
31 if hasattr(config_parser, 'sections') and \
32 hasattr(config_parser, 'options'):
33 config_obj = ConfigDbFile(config_parser)
34 self._add_connections(config_obj)
35 else:
36 raise NotConfigParserObject()
37
38 def _add_connections(self, config_obj):
39 connections = config_obj()
40 for name, obj in connections.items():
41 self.connections[name] = obj
42
43 def load(self, name):
44 if name in self.connections:
45 connection = self.connections[name]
46 if connection.db_obj:
47 return connection.db_obj
48 else:
49 handler = self._load_handler(connection.handler)
50 db_obj = handler.load(connection)
51 self.connections[name].db_obj = db_obj
52 return db_obj
53
54 def _load_handler(self, name):
55 if name in self._handlers:
56 handler = self._handlers[name]
57 cls = handler[0]
58 obj = handler[1]
59 if not obj:
60 obj = cls()
61 self._handlers[name] = [cls, obj]
62 return obj
63 raise UnknownHandlerException(name)
64
65
66
67class ConfigDbFile(object):
68
69 OPTIONS_REQUIRED = [
70 ['protocol', 'hostname', 'port', 'username', 'password','database'],
71 ['protocol', 'filename']
72 ]
73 OPTIONS_EXTRA = ['protocol_extra', 'handler']
74 DEFAULT_HANDLER = 'sa'
75
76 def __init__(self, config_parser):
77 self.parser = config_parser
78 self._options = self._all_options()
79 self.connections = {}
80
81 def __call__(self):
82 if not self.connections:
83 for section in self.parser.sections():
84 if self._is_valid(section):
85 obj = self._build_connection(section)
86 self.connections[obj.name] = obj
87 return self.connections
88
89 def _all_options(self):
90 out = []
91 for option_group in self.OPTIONS_REQUIRED:
92 for option in option_group:
93 if option not in out:
94 out.append(option)
95 for option in self.OPTIONS_EXTRA:
96 if option not in out:
97 out.append(option)
98 return out
99
100 def _is_valid(self, section):
101 for option_group in self.OPTIONS_REQUIRED:
102 total = len(option_group)
103 count = 0
104 for option in option_group:
105 if option in self.parser.options(section):
106 value = self.parser.get(section, option)
107 if value:
108 count += 1
109 if count >= total:
110 return True
111 return False
112
113 def _build_connection(self, section):
114 obj = ConfigDbConnection()
115 for option in self._options:
116 obj.name = section
117 if self.parser.has_option(section, option):
118 value = self.parser.get(section, option)
119 setattr(obj, option, value)
120 if not hasattr(obj, 'handler') or not obj.handler:
121 obj.handler = self.DEFAULT_HANDLER
122 return obj
123
124
125class BaseConfigDb(object):
126
127 PROTOCOLS = ['postgresql', 'mysql', 'sqlite', 'mssql', 'oracle']
128
129 def _clean(self, obj):
130 obj.protocol = self._clean_protocol(obj.protocol)
131 if hasattr(obj, 'port'):
132 obj.port = self._clean_port(obj.port)
133
134 def _clean_protocol(self, data):
135 data = data.lower()
136 if data in ('postgres', 'postgre'):
137 data = 'postgresql'
138 if data not in self.PROTOCOLS:
139 raise InvalidProtocolException(data)
140 else:
141 return data
142
143 def _clean_port(self, data):
144 try:
145 data = int(data)
146 except ValueError:
147 data = None
148 except TypeError:
149 data = None
150 if data:
151 if data <1 or data > 65535:
152 raise InvalidPortException(data)
153 return data
154
155
156class ConfigDbConnection(object):
157
158 def __init__(self):
159 self.db_obj = None
160
161
162class NotConfigParserObject(Exception):
163 pass
164
165
166class InvalidProtocolException(Exception):
167 pass
168
169
170class InvalidPortException(Exception):
171 pass
172
173
174class UnknownHandlerException(Exception):
175 pass
176
177
178class UnknownConnectionException(Exception):
179 pass
diff --git a/dodai/config/db/sa.py b/dodai/config/db/sa.py
new file mode 100644
index 0000000..06adf9c
--- /dev/null
+++ b/dodai/config/db/sa.py
@@ -0,0 +1,64 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18from dodai.config.db import BaseConfigDb
19from dodai.db import Db
20
21class Sa(BaseConfigDb):
22
23
24 def load(self, obj):
25 from sqlalchemy.orm import sessionmaker
26 self._clean(obj)
27 db = Db()
28 db.engine = self._build_engine(obj)
29 Session = sessionmaker(bind=db.engine)
30 db.session = Session()
31 db.name = obj.name
32 db.protocol = obj.protocol
33 if hasattr(obj, 'filename') and obj.filename:
34 db.filename = obj.filename
35 else:
36 db.hostname = obj.hostname
37 if hasattr(obj, 'port') and obj.port:
38 db.port = obj.port
39 db.database = obj.database
40 return db
41
42 def _build_connection_string(self, obj):
43 out = []
44 out.append('{db.protocol}')
45 if hasattr(obj, 'protocol_extra') and obj.protocol_extra:
46 out.append('+{db.protocol_extra}')
47 out.append('://')
48 if hasattr(obj, 'filename') and obj.filename:
49 out.append('{db.filename}')
50 else:
51 out.append('{db.username}:{db.password}@')
52 out.append('{db.hostname}')
53 if hasattr(obj, 'port') and obj.port:
54 out.append(':{db.port}')
55 out.append('/{db.database}')
56 out = ''.join(out)
57 out = out.format(db=obj)
58 return out
59
60 def _build_engine(self, obj):
61 from sqlalchemy import create_engine
62 connection_string = self._build_connection_string(obj)
63 db_obj = create_engine(connection_string)
64 return db_obj
diff --git a/dodai/config/file.py b/dodai/config/file.py
new file mode 100644
index 0000000..5cab6f8
--- /dev/null
+++ b/dodai/config/file.py
@@ -0,0 +1,116 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19import os
20import ConfigParser
21
22class ConfigFile(object):
23
24 def __init__(self):
25 self._files = []
26 self._parser = None
27 self.files_loaded = []
28 self._dir = None
29
30 def set_directory(self, path):
31 """ Sets the direcory where files will be looked for
32 raises: InvalidDirectoryException or DirectoryDoesNotExistException
33 """
34 if os.path.isdir(path):
35 self._dir = path
36 else:
37 if os.path.isfile(path):
38 raise InvalidDirectoryException(path)
39 else:
40 raise DirectoryDoesNotExistException(path)
41
42 def get_directory(self):
43 """ Returns the directory where files will be looked for
44 """
45 return self._dir
46
47 def add_file(self, path):
48 """ Adds a full file path with the given path (list or string)
49 raises: FileDoesNotExistException
50 """
51 if isinstance(path, list):
52 for file_ in path:
53 self._add_file(file_)
54 else:
55 if path not in self._files:
56 self._add_file(path)
57
58 def _add_file(self, path):
59 """ Adds the given file path file to the object if the filepath
60 doesn't already exist
61 """
62 if os.path.isfile(path):
63 if path not in self._files:
64 self._files.append(path)
65 else:
66 raise FileDoesNotExistException(path)
67
68 def get_files(self):
69 """ Returns a list of files that were added to this object
70 """
71 return self._files
72
73 def parser(self):
74 """ Returns a ConfigParser.ConfigParser object with files loaded
75 raises: NoFilesToLoadException
76 """
77 self._reset_parser()
78 if not self._parser:
79 if not self._files:
80 raise NoFilesToLoadException()
81 self._parser = ConfigParser.ConfigParser()
82 self.files_loaded = self._parser.read(self._files)
83 return self._parser
84
85 def load(self, name):
86 """ Takes the given name and merges it with the object's directory
87 then adds the path to the object
88 """
89 if not self._dir:
90 raise DirectoryNotSetException()
91 else:
92 path = os.path.join(self._dir, name)
93 self.add_file(path)
94
95 def _reset_parser(self):
96 """ Resets the _parser property if the files_loaded does not equal
97 the files assigned to this object
98 """
99 if self._parser:
100 if self.files_loaded != self._files:
101 self._parser = None
102
103class NoFilesToLoadException(Exception):
104 pass
105
106class DirectoryNotSetException(Exception):
107 pass
108
109class InvalidDirectoryException(Exception):
110 pass
111
112class DirectoryDoesNotExistException(Exception):
113 pass
114
115class FileDoesNotExistException(Exception):
116 pass
diff --git a/dodai/config/log.py b/dodai/config/log.py
new file mode 100644
index 0000000..fdb5c93
--- /dev/null
+++ b/dodai/config/log.py
@@ -0,0 +1,150 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19import logging
20import logging.handlers
21import os
22
23class ConfigLog(object):
24
25 LEVELS = {
26 logging.CRITICAL: [
27 'critical',
28 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29 "%(message)s"],
30 logging.ERROR: [
31 'error',
32 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
33 "%(message)s"],
34 logging.WARNING: [
35 'warning',
36 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
37 "%(message)s"],
38 logging.INFO: [
39 'info',
40 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
41 "%(message)s"],
42 logging.DEBUG: [
43 'debug',
44 "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
45 "%(message)s"]
46 }
47
48 MAX_BYTES = 10485760
49 BACKUP_COUNT = 5
50 FILE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
51 STDOUT_FORMAT = "%(message)s"
52
53 def __init__(self):
54 self.log_level = logging.CRITICAL
55 self.directory = None
56 self._levels = {}
57
58 def set_log_level(self, level):
59 try:
60 level = self._fetch_log_level(level)
61 except InvalidLevelException:
62 pass
63 else:
64 self.log_level = level
65
66 def set_directory(self, directory):
67 if os.path.isdir(directory):
68 self.directory = directory
69 else:
70 raise NoDirectoryExistException(directory)
71
72 def get_file_message_format(self, level):
73 if not self._levels:
74 self._levels = self.LEVELS
75 level = self._fetch_log_level(level)
76 return self._levels[level][1]
77
78 def get_screen_message_format(self, level):
79 if not self._levels:
80 self._levels = self.LEVELS
81 level = self._fetch_log_level(level)
82 return self._levels[level][2]
83
84 def _fetch_log_level(self, level):
85 out = None
86 if isinstance(level, str):
87 level = level.strip().lower()
88 if level in self.LEVELS:
89 out = level
90 else:
91 for key, items in self.LEVELS.items():
92 if level == items[0]:
93 out = key
94 if not out:
95 raise InvalidLevelException(level)
96 else:
97 return out
98
99 def _build_filepath(self, data):
100 data = os.path.normpath(data)
101 if data.startswith(os.path.sep):
102 dir = os.path.dirname(data)
103 if not os.path.isdir(dir):
104 raise NoDirectoryExistException(dir)
105 else:
106 if not self.directory:
107 raise DirectoryNotSetException()
108 else:
109 data = os.path.join(self.directory, data)
110 return data
111
112 def load(self, name):
113 log =logging.getLogger(name)
114 log.setLevel(self.log_level)
115 return log
116
117 def attach_file_handler(self, log, filename):
118 filepath = self._build_filepath(filename)
119 handler = logging.handlers.RotatingFileHandler(
120 filepath, maxBytes = self.MAX_BYTES,
121 backupCount=self.BACKUP_COUNT)
122 file_format = self.get_file_message_format(self.log_level)
123 format_obj = logging.Formatter(file_format)
124 handler.setFormatter(format_obj)
125 handler.setLevel(self.log_level)
126 log.addHandler(handler)
127
128 def attach_screen_handler(self, log, level=None):
129 if level:
130 level = self._fetch_log_level(level)
131 else:
132 level = self.log_level
133 message_format = self.get_screen_message_format(level)
134 handler = logging.StreamHandler()
135 handler.setLevel(level)
136 format_obj = logging.Formatter(message_format)
137 handler.setFormatter(format_obj)
138 log.addHandler(handler)
139
140
141class NoDirectoryExistException(Exception):
142 pass
143
144
145class DirectoryNotSetException(Exception):
146 pass
147
148
149class InvalidLevelException(Exception):
150 pass
diff --git a/dodai/config/option.py b/dodai/config/option.py
new file mode 100644
index 0000000..0561881
--- /dev/null
+++ b/dodai/config/option.py
@@ -0,0 +1,61 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18
19from optparse import OptionParser
20
21class ConfigOption(object):
22
23 def __init__(self):
24
25 self.parser = OptionParser()
26 self._options = None
27 self._args = []
28
29 def get_args(self):
30 self._parse_args()
31 return self._args
32
33 def get_options(self):
34 self._parse_args()
35 return self._options
36
37 def _parse_args(self):
38 options, args = self.parser.parse_args()
39 self._options = options
40 self._args = args
41
42 def add_quiet(self):
43
44 self.parser.add_option("-q", "--quiet", dest="verbose", default=True,
45 action="store_false",
46 help="Don't print status messages to the screen")
47
48 def add_verbose(self):
49 self.parser.add_option("-v", "--verbose", dest="verbose",
50 action="store_true",
51 default=False, help="Print status messages to the screen")
52
53 def add_log_level(self, default='critical'):
54 self.parser.add_option("-l", "--log-level", dest="log_level",
55 default=default, help="Sets the log level")
56
57 def add_setup(self):
58 self.parser.add_option('', "--setup", dest="setup",
59 action="store_true", default=False,
60 help="run the setup which builds the config "\
61 "files.")
diff --git a/dodai/config/sections.py b/dodai/config/sections.py
new file mode 100644
index 0000000..feb59be
--- /dev/null
+++ b/dodai/config/sections.py
@@ -0,0 +1,172 @@
1# Copyright (C) 2010 Leonard Thomas
2#
3# This file is part of Dodai.
4#
5# Dodai is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Dodai is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Dodai. If not, see <http://www.gnu.org/licenses/>.
17
18import unicodedata
19
20class ConfigSections(object):
21 """
22 An iterable object that contains ConfigSection objects
23
24 """
25
26 def __init__(self, string_object = None):
27 """
28 Iterable object that handles the conversion of a config
29 parser object to a list of section objects.
30
31
32 string_object: This is an object (non instantiated or
33 callable) that the results of the config's
34 sections, and options will be stored in.
35 This enables you to store your values as a
36 custom object. A good object to use is the
37 dodai.tools.himo Himo object. If the
38 string_object is not given the default python
39 str() object will be used.
40
41 """
42 self._string_object = string_object or None
43 self._sections = {}
44
45 def __call__(self, parser):
46 """
47 Parses the given parser object into this object's sections.
48
49 parser: The actual parser object that is used to
50 get the sections. This object must have
51 the sections(), options() and get()
52 methods. A good object to use is the native
53 ConfigParse object. However, you can create
54 your own
55
56 """
57 self._build_sections(parser)
58
59 def _build_sections(self, parser):
60 # Builds ConfigSection objects from the parser object
61
62 for section_name in parser.sections():
63 section = self.get_section(section_name)
64 self._build_options(parser, section_name, section)
65
66 def _build_options(self, parser, section_name, section):
67 # Adds the options to the section object
68
69 for key in parser.options(section_name):
70 key = unicode(self._build_string_object(key))
71 value = self._build_string_object(parser.get(section_name, key))
72 setattr(section, key, value)
73
74 def _build_string_object(self, data):
75 if self._string_object:
76 return self._string_object(data)
77 else:
78 return data
79
80 def get_section(self, section_name):
81 """
82 Returns a ConfigSection object from this object's section
83 dictionary or creates a new ConfigSection object, which is
84 stored int this object's section dictionary then is returned
85
86 """
87 section_name = unicode(self._build_string_object(section_name))
88 if section_name in self._sections:
89 return self._sections[section_name]
90 else:
91 section = ConfigSection(section_name)
92 self._sections[section_name] = section
93 return section
94
95 def __getitem__(self, key):
96 key = normalize_key(key)
97 return self._sections[key]
98
99 def __getattr__(self, key):
100 key = normalize_key(key)
101 try:
102 out = self._sections[key]
103 except KeyError:
104 return getattr(self._sections, key)
105 else:
106 return out
107
108 def __iter__(self, *args, **kargs):
109 return self._sections.__iter__(*args, **kargs)
110
111
112 def __len__(self):
113 return len(self._sections)
114
115class ConfigSection(object):
116 """
117 A generic object to hold keys and values primarily from a config file
118
119 """
120 def __init__(self, title):
121 """
122 Holds keys and values primarily from a section of a config file
123
124 title: The title of the section of the config file
125
126 """
127 self.___title___ = title
128 self.___options___ = {}
129
130
131 def get_title(self):
132 """
133 Returns the title of the section
134
135 """
136 return self.___title___
137
138
139 def __setattr__(self, key, value):
140 if key.startswith('___') and key.endswith('___'):
141 object.__setattr__(self, key, value)
142 else:
143 key = normalize_key(key)
144 if self.___options___.has_key(key):
145 self.___options___[key] = value
146 else:
147 dict.__setitem__(self.___options___, key, value)
148
149 def __getattr__(self, key):
150 if key.startswith('___') and key.endswith('___'):
151 return self.__dict__[key]
152 else:
153 key = normalize_key(key)
154 try:
155 out = self.___options___[key]
156 except KeyError:
157 return getattr(self.___options___, key)
158 else:
159 return out
160
161 def __getitem__(self, key):
162 key = normalize_key(key)
163 return self.___options___[key]
164
165 def __iter__(self, *args, **kargs):
166 return self.___options___.__iter__(*args, **kargs)
167
168
169def normalize_key(key):
170 key = unicode(key)
171 key = unicodedata.normalize('NFC', key)
172 return key