diff options
author | Wojciech Polak <polak@gnu.org> | 2009-03-08 00:36:15 +0100 |
---|---|---|
committer | Wojciech Polak <polak@gnu.org> | 2009-03-08 17:30:10 +0100 |
commit | aec2ae6a91539e5f20ef7230b7a2e0f3578286e7 (patch) | |
tree | cbc22f2f64d6e33bdfa9ff3f4cf32b86fcdf8bec /dicoweb | |
parent | 660a9a4fdfbe35df77eb9457bd0cc473b8cbbc95 (diff) | |
download | dico-aec2ae6a91539e5f20ef7230b7a2e0f3578286e7.tar.gz dico-aec2ae6a91539e5f20ef7230b7a2e0f3578286e7.tar.bz2 |
Add Dicoweb -- a web search interface.
Diffstat (limited to 'dicoweb')
26 files changed, 2034 insertions, 0 deletions
diff --git a/dicoweb/INSTALL b/dicoweb/INSTALL new file mode 100644 index 0000000..e213131 --- /dev/null +++ b/dicoweb/INSTALL @@ -0,0 +1,80 @@ +GNU Dico - Dicoweb INSTALL +Copyright (C) 2008, 2009 Wojciech Polak + +* Dicoweb requirements +====================== + +- Django 1.0 -- a Python Web framework (http://www.djangoproject.com/) +- Memcached -- a distributed memory object caching system + (http://www.danga.com/memcached/) alongside python-memcached module. +- Wit -- a wiki translator distributed within GNU Dico. + (http://puszcza.gnu.org.ua/projects/wit/) + +* Installation instructions +=========================== + +Edit your local Dicoweb settings in the file 'dicoweb/settings.py'. + + +** The development/test server +------------------------------ + +Change the current working directory into the `dicoweb' directory +and run the command `python manage.py runserver'. You will see +the following output: + + Validating models... + 0 errors found. + + Django version 1.0, using settings 'dicoweb.settings' + Development server is running at http://127.0.0.1:8000/ + Quit the server with CONTROL-C. + +** Production server with mod_wsgi +---------------------------------- + +Apache configuration: + + LoadModule wsgi_module modules/mod_wsgi.so + WSGIScriptAlias / /usr/local/django/dicoweb/dicoweb.wsgi + Alias /static "/usr/local/django/dicoweb/static" + +More detailed information is available at: +http://code.google.com/p/modwsgi/wiki/IntegrationWithDjango + +** Production server with mod_python +------------------------------------ + +Apache configuration: + + LoadModule python_module modules/mod_python.so + <Location "/"> + SetHandler python-program + PythonHandler django.core.handlers.modpython + PythonPath "sys.path + ['/usr/local/django', '/usr/local/django/dicoweb']" + SetEnv DJANGO_SETTINGS_MODULE dicoweb.settings + PythonInterpreter dicoweb + PythonDebug Off + </Location> + + <Location "/static"> + SetHandler None + </Location> + <Location "/favicon.ico"> + SetHandler None + </Location> + + <Directory "/usr/local/django/dicoweb/"> + AllowOverride All + Options None + Order allow,deny + Allow from all + </Directory> + + + +Local Variables: +mode: outline +paragraph-separate: "[ ]*$" +version-control: never +End: diff --git a/dicoweb/__init__.py b/dicoweb/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/dicoweb/__init__.py diff --git a/dicoweb/dicoclient/__init__.py b/dicoweb/dicoclient/__init__.py new file mode 100644 index 0000000..1091705 --- /dev/null +++ b/dicoweb/dicoclient/__init__.py @@ -0,0 +1,19 @@ +# This file is part of GNU Dico. +# Copyright (C) 2008, 2009 Wojciech Polak +# +# GNU Dico is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Dico is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Dico. If not, see <http://www.gnu.org/licenses/>. + +__all__ = ["dicoclient"] + +from dicoclient import * diff --git a/dicoweb/dicoclient/dicoclient.py b/dicoweb/dicoclient/dicoclient.py new file mode 100644 index 0000000..75b5bd2 --- /dev/null +++ b/dicoweb/dicoclient/dicoclient.py @@ -0,0 +1,392 @@ +# This file is part of GNU Dico. +# Copyright (C) 2008, 2009 Wojciech Polak +# +# GNU Dico is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Dico is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Dico. If not, see <http://www.gnu.org/licenses/>. + +import socket +import select +import re +import base64 +import quopri + +__version__ = "1.0" + +class DicoClient: + """GNU Dico client module written in Python + (a part of GNU Dico software)""" + + host = None; + levenshtein_distance = 0 + mime = False + + verbose = 0 + timeout = 10 + transcript = False + __connected = False + + def __init__ (self, host = None): + if host != None: + self.host = host; + + def __del__ (self): + if self.__connected: + self.socket.close () + + def open (self, host = None, port = 2628): + """Open the connection to the DICT server.""" + if host != None: + self.host = host + if self.verbose: + self.__debug ("Connecting to %s:%d" % (self.host, port)) + socket.setdefaulttimeout (int (self.timeout)) + self.socket = socket.socket (socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect ((self.host, port)) + self.__connected = True + self.fd = self.socket.makefile (); + + self.server_banner = self.__read ()[0] + capas, msgid = re.search ('<(.*)> (<.*>)$', + self.server_banner).groups () + self.server_capas = capas.split ('.') + self.server_msgid = msgid + + self.__send_client () + self.__read () + + def close (self): + """Close the connection.""" + if self.__connected: + self.__send_quit () + self.__read () + self.socket.close () + self.__connected = False + + def option (self, name, *args): + """Send the OPTION command.""" + if self.__connected: + self.__send ('OPTION %s%s' % + (name, reduce (lambda x, y: str (x) +' '+ str (y), + args, ''))) + res = self.__read () + code, msg = res[0].split (' ', 1) + if int (code) == 250: + if name.lower () == 'mime': + self.mime = True + return True + return False + + def __get_mime (self, lines): + cnt = 0 + mimeinfo = {} + firstline = lines[0].lower () + if firstline.find ('content-type:') != -1 or \ + firstline.find ('content-transfer-encoding:') != -1: + cnt += 1 + for line in lines: + if line == '': + break + t = line.split (':', 1) + mimeinfo[t[0].lower ()] = t[1].strip () + cnt += 1 + for i in range (0, cnt): + lines.pop (0) + else: + lines.pop (0) + if 'content-transfer-encoding' in mimeinfo: + if mimeinfo['content-transfer-encoding'].lower () == 'base64': + buf = base64.decodestring ('\n'.join (lines)) + lines[:] = (buf.split ('\r\n')) + if lines[-1] == '': del lines[-1] + del mimeinfo['content-transfer-encoding'] + elif mimeinfo['content-transfer-encoding'].lower () == 'quoted-printable': + buf = quopri.decodestring ('\n'.join (lines)) + lines[:] = buf.split ('\r\n') + if lines[-1] == '': del lines[-1] + del mimeinfo['content-transfer-encoding'] + return mimeinfo + + def __get_rs (self, line): + code, text = line.split (' ', 1) + code = int (code) + return code, text + + def __read (self): + if not self.__connected: + raise DicoNotConnectedError ('Not connected') + buf = [] + line = self.__readline () + if len (line) == 0: + raise DicoNotConnectedError ('Not connected') + buf.append (line) + code, text = self.__get_rs (line) + + if code >= 100 and code < 200: + if code == 150: + while True: + rs = self.__readline () + code, text = self.__get_rs (rs) + if (code != 151): + buf.append (rs) + break + buf.append ([rs, self.__readblock ()]) + else: + buf.append (self.__readblock ()) + buf.append (self.__readline ()) + return buf + + def __readline (self): + line = self.fd.readline ().rstrip () + if self.transcript: + self.__debug ("S:%s" % line) + return line + + def __readblock (self): + buf = [] + while True: + line = self.__readline () + if line == '.': + break + buf.append (line) + return buf + + + def __send (self, command): + if not self.__connected: + raise DicoNotConnectedError ('Not connected') + self.socket.send (command.encode ('utf_8') + "\r\n") + if self.transcript: + self.__debug ("C:%s" % command) + + def __send_client (self): + if self.verbose: + self.__debug ("Sending client information") + self.__send ('CLIENT "%s %s"' % ("GNU Dico (Python Edition)", + __version__)) + + def __send_quit (self): + if self.verbose: + self.__debug ("Quitting") + self.__send ('QUIT'); + + def __send_show (self, what, arg = None): + if arg != None: + self.__send ('SHOW %s "%s"' % (what, arg)) + else: + self.__send ('SHOW %s' % what) + return self.__read () + + def __send_define (self, database, word): + if self.verbose: + self.__debug ('Sending query for word "%s" in database "%s"' + % (word, database)) + self.__send ('DEFINE "%s" "%s"' % (database, word)) + return self.__read () + + def __send_match (self, database, strategy, word): + if self.verbose: + self.__debug ('Sending query to match word "%s" in database "%s", using "%s"' + % (word, database, strategy)) + self.__send ('MATCH "%s" "%s" "%s"' % (database, strategy, word)) + return self.__read () + + def __send_xlev (self, distance): + self.__send ('XLEV %u' % distance) + return self.__read () + + def show_databases (self): + """List all accessible databases.""" + if self.verbose: + self.__debug ("Getting list of databases") + res = self.__send_show ('DATABASES') + if (self.mime): + mimeinfo = self.__get_mime (res[1]) + dbs_res = res[1:-1][0] + dbs = [] + for d in dbs_res: + short_name, full_name = d.split (' ', 1) + dbs.append ([short_name, self.__unquote (full_name)]) + dct = { + "count": len (dbs), + "databases": dbs + } + return dct + + def show_strategies (self): + """List available matching strategies.""" + if self.verbose: + self.__debug ("Getting list of strategies") + res = self.__send_show ('STRATEGIES') + if (self.mime): + mimeinfo = self.__get_mime (res[1]) + sts_res = res[1:-1][0] + sts = [] + for s in sts_res: + short_name, full_name = s.split (' ', 1) + sts.append ([short_name, self.__unquote (full_name)]) + dct = { + "count": len (sts), + "strategies": sts + } + return dct + + def show_info (self, database): + """Provide information about the database.""" + res = self.__send_show ("INFO", database) + code, msg = res[0].split (' ', 1) + if int (code) < 500: + if (self.mime): + mimeinfo = self.__get_mime (res[1]) + dsc = res[1] + return { "desc": '\n'.join (dsc) } + else: + return { "error": code, "msg": msg } + + def show_lang_db (self): + """Show databases with their language preferences.""" + res = self.__send_show ("LANG DB") + code, msg = res[0].split (' ', 1) + if int (code) < 500: + if (self.mime): + mimeinfo = self.__get_mime (res[1]) + dsc = res[1] + lang_src = {} + lang_dst = {} + for i in dsc: + pair = i.split (' ', 1)[1] + src, dst = pair.split (':', 1) + for j in src: + lang_src[src.strip()] = True + for j in dst: + lang_dst[dst.strip()] = True + return { "desc": '\n'.join (dsc), + "lang_src": lang_src.keys (), + "lang_dst": lang_dst.keys () } + else: + return { "error": code, "msg": msg } + + def show_lang_pref (self): + """Show server language preferences.""" + res = self.__send_show ("LANG PREF") + code, msg = res[0].split (' ', 1) + if int (code) < 500: + return { "msg": msg } + else: + return { "error": code, "msg": msg } + + def show_server (self): + """Provide site-specific information.""" + res = self.__send_show ('SERVER') + code, msg = res[0].split (' ', 1) + if int (code) < 500: + dsc = res[1] + return { "desc": '\n'.join (dsc) } + else: + return { "error": code, "msg": msg } + + def define (self, database, word): + """Look up word in database.""" + database = database.replace ('"', "\\\"") + word = word.replace ('"', "\\\"") + res = self.__send_define (database, word) + code, msg = res[-1].split (' ', 1) + if int (code) < 500: + defs_res = res[1:-1] + defs = [] + rx = re.compile ('^\d+ ("[^"]+"|\w+) ([a-zA-Z0-9_\-]+) ("[^"]+"|\w+)') + for i in defs_res: + term, db, db_fullname = rx.search (i[0]).groups () + df = {"term": self.__unquote (term), + "db": db, + "db_fullname": self.__unquote (db_fullname)} + if (self.mime): + mimeinfo = self.__get_mime (i[1]) + df.update (mimeinfo) + df['desc'] = '\n'.join (i[1]) + defs.append (df) + dct = { + "count": len (defs), + "definitions": defs + } + return dct + else: + return { "error": code, "msg": msg } + + def match (self, database, strategy, word): + """Match word in database using strategy.""" + if not self.__connected: + raise DicoNotConnectedError ('Not connected') + + if self.levenshtein_distance and 'xlev' in self.server_capas: + res = self.__send_xlev (self.levenshtein_distance) + code, msg = res[-1].split (' ', 1) + if int (code) != 250 and self.verbose: + self.__debug ("Server rejected XLEV command") + self.__debug ("Server reply: %s" % msg) + + database = database.replace ('"', "\\\"") + strategy = strategy.replace ('"', "\\\"") + word = word.replace ('"', "\\\"") + + res = self.__send_match (database, strategy, word) + code, msg = res[-1].split (' ', 1) + if int (code) < 500: + if (self.mime): + mimeinfo = self.__get_mime (res[1]) + mts_refs = res[1:-1][0] + mts = {} + for i in mts_refs: + db, term = i.split (' ', 1) + if mts.has_key (db): + mts[db].append (self.__unquote (term)) + else: + mts[db] = [self.__unquote (term)] + dct = { + "matches": mts + } + return dct + else: + return { "error": code, "msg": msg } + + def xlev (self, distance): + """Set Levenshtein distance.""" + self.levenshtein_distance = distance + res = self.__send_xlev (distance) + code, msg = res[0].split (' ', 1) + if int (code) == 250: + return True + return False + + def __unquote (self, s): + s = s.replace ("\\\\'", "'") + if s[0] == '"' and s[-1] == '"': + s = s[1:-1] + try: + s = self.__decode (s) + except UnicodeEncodeError: + pass + return s + + def __decode (self, encoded): + for octc in (c for c in re.findall (r'\\(\d{3})', encoded)): + encoded = encoded.replace (r'\%s' % octc, chr (int (octc, 8))) + return unicode (encoded, 'utf_8') + + def __debug (self, msg): + print "dico: Debug: %s" % msg + +class DicoNotConnectedError (Exception): + def __init__ (self, value): + self.parameter = value + def __str__(self): + return repr (self.parameter) diff --git a/dicoweb/dicoclient/dicoshell.py b/dicoweb/dicoclient/dicoshell.py new file mode 100644 index 0000000..dfec3fc --- /dev/null +++ b/dicoweb/dicoclient/dicoshell.py @@ -0,0 +1,321 @@ +# This file is part of GNU Dico. +# Copyright (C) 2008, 2009 Wojciech Polak +# +# GNU Dico is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Dico is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Dico. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import atexit +import re +import getopt +import readline +import curses.ascii +import socket +import dicoclient + +class Shell: + """Simple GNU Dico-Python Shell.""" + + prompt = 'dico> ' + prefix = '.' + + default_host = 'gnu.org.ua' + database = '!' + strategy = '.' + last_matches = [] + last_databases = [] + last_strategies = [] + transcript = False + + def __init__ (self, opts, args): + for o, a in opts: + if o in ('-h', '--host'): + self.default_host = a + + self.dc = dicoclient.DicoClient (self.default_host) + + def run (self): + histfile = os.path.expanduser ('~/.dico_history') + try: + readline.read_history_file (histfile) + except IOError: + pass + atexit.register (readline.write_history_file, histfile) + + print "\nType ? for help summary\n" + while True: + try: + input = raw_input (self.prompt).strip () + input = unicode (input, 'utf_8') + except (EOFError, KeyboardInterrupt): + print + sys.exit () + + try: + self.parse (input) + except socket.timeout: + self.__error ('socket timed out') + except dicoclient.DicoNotConnectedError: + try: + self.dc.open () + dict = self.dc.show_databases () + self.last_databases = dict['databases'] + dict = self.dc.show_strategies () + self.last_strategies = dict['strategies'] + self.parse (input) + except socket.error, (errno, strerror): + self.__error (strerror) + + def parse (self, input): + if len (input) < 1: + return + if input[0] == self.prefix: + self.parse_command (input[1:]) + elif input == '?': + self.print_help () + elif re.match (r'^[0-9]+$', input): + try: + match = self.last_matches[int (input)] + dict = self.dc.define (match[0], match[1]) + if dict.has_key ('count'): + for d in dict['definitions']: + print "From %s, %s:" % (d['db'], d['db_fullname']. + encode ('utf_8')) + print d['desc'] + elif dict.has_key ('error'): + print dict['msg'] + except IndexError: + self.__error ('No previous match') + elif input[0] == '/': + if len (input) > 1: + dict = self.dc.match (self.database, self.strategy, input[1:]) + if dict.has_key ('matches'): + self.last_matches = [] + lmi = 0 + for db in dict['matches']: + print "From %s, %s:" % (db, self.__lookup_db (db). + encode ('utf_8')) + for term in dict['matches'][db]: + print '%4d) "%s"' % (lmi, term.encode ('utf_8')) + self.last_matches.append ([db, term]) + lmi = lmi + 1 + elif dict.has_key ('error'): + print dict['msg'] + else: + if len (self.last_matches) > 0: + m = {} + lmi = 0 + for i, db in enumerate (self.last_matches): + if not db[0] in m: m[db[0]] = [] + m[db[0]].append (self.last_matches[i][1]) + for db in m: + print "From %s, %s:" % (db, self.__lookup_db (db)) + for term in m[db]: + print '%4d) "%s"' % (lmi, term) + lmi = lmi + 1 + else: + self.__error ('No previous match') + elif input[0] == '!': + if re.match (r'^![0-9]+$', input): + number = int (input[1:]) + readline.insert_text (readline.get_history_item (number)) + readline.redisplay () + else: + dict = self.dc.define (self.database, input) + if dict.has_key ('count'): + for d in dict['definitions']: + print "From %s, %s:" % (d['db'], d['db_fullname']. + encode ('utf_8')) + print d['desc'] + elif dict.has_key ('error'): + print dict['msg'] + + def parse_command (self, input): + input = input.split (' ', 1) + cmd = input[0] + args = None + if len (input) == 2: + args = input[1] + + if cmd == 'open': + try: + if args != None: + args = args.split (' ', 1) + if len (args) == 2: + self.dc.open (args[0], int (args[1])) + else: + self.dc.open (args[0]) + else: + self.dc.open () + dict = self.dc.show_databases () + self.last_databases = dict['databases'] + dict = self.dc.show_strategies () + self.last_strategies = dict['strategies'] + except socket.error, (errno, strerror): + self.__error (strerror) + elif cmd == 'close': + self.dc.close () + elif cmd == 'database': + if args != None: + self.database = args + else: + print self.database + elif cmd == 'strategy': + if args != None: + self.strategy = args + else: + print self.strategy + elif cmd == 'distance': + if args != None: + self.dc.levenshtein_distance = int (args) + else: + if self.dc.levenshtein_distance: + print "Configured Levenshtein distance: %u" \ + % (self.dc.levenshtein_distance) + else: + print "No distance configured" + elif cmd == 'ls': + dict = self.dc.show_strategies () + self.last_strategies = dict['strategies'] + if len (self.last_strategies): + for i in self.last_strategies: + print '%s "%s"' % (i[0], i[1]) + elif cmd == 'ld': + dict = self.dc.show_databases () + self.last_databases = dict['databases'] + if len (self.last_databases): + for i in self.last_databases: + print '%s "%s"' % (i[0], i[1]) + elif cmd == 'mime': + print self.dc.option ('MIME') + elif cmd == 'server': + dict = self.dc.show_server () + if dict.has_key ('desc'): + print dict['desc'] + elif dict.has_key ('error'): + self.__error (dict['error'] + ' ' + dict['msg']) + elif cmd == 'info': + if args != None: + dict = self.dc.show_info (args) + if dict.has_key ('desc'): + print dict['desc'] + elif dict.has_key ('error'): + self.__error (dict['error'] + ' ' + dict['msg']) + elif cmd == 'history': + hl = int (readline.get_current_history_length ()) + for i in range (0, hl): + print "%4d) %s" % (i, readline.get_history_item (i)) + elif cmd == 'help': + self.print_help () + elif cmd == 'transcript': + if args != None: + if args == 'yes' or args == 'on' or args == 'true': + self.dc.transcript = True + elif args == 'no' or args == 'off' or args == 'false': + self.dc.transcript = False + else: + self.__error ('Expected boolean value') + else: + if self.dc.transcript: + print "transcript is on" + else: + print "transcript is off" + elif cmd == 'verbose': + if args != None: + self.dc.verbose = args + else: + print self.dc.verbose + elif cmd == 'prompt': + if args != None: + self.prompt = args + else: + self.__error ('not enough arguments') + elif cmd == 'prefix': + if args != None: + if len (args) == 1 and args != '#' and \ + curses.ascii.ispunct (args): + self.prefix = args + else: + self.__error ('Expected a single punctuation character') + elif cmd == 'version': + self.print_version () + elif cmd == 'warranty': + self.print_warranty () + elif cmd == 'quit': + sys.exit () + + def __lookup_db (self, db): + for d in self.last_databases: + if d[0] == db: + return d[1] + return "" + + def __error (self, msg): + print "dico: Error: %s" % msg + + def print_version (self): + print "GNU Dico (Python Edition) " + dicoclient.__version__ + + def print_warranty (self): + self.print_version () + print """Copyright (C) 2008, 2009 Wojciech Polak + + GNU Dico is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + GNU Dico is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GNU Dico. If not, see <http://www.gnu.org/licenses/>.""" + + def print_help (self): + print 'WORD Define WORD.' + print '/WORD Match WORD.' + print '/ Redisplay previous matches.' + print 'NUMBER Define NUMBERth match.' + print '!NUMBER Edit NUMBERth previous command.' + print + print self.prefix + 'open [HOST [PORT]] Connect to a DICT server.' + print self.prefix + 'close Close the connection.' + print self.prefix + 'database [NAME] Set or display current database name.' + print self.prefix + 'strategy [NAME] Set or display current strategy.' + print self.prefix + 'distance [NUM] Set or query Levenshtein distance (server-dependent).' + print self.prefix + 'ls List available matching strategies' + print self.prefix + 'ld List all accessible databases' + print self.prefix + 'info [DB] Display the information about the database.' + print self.prefix + 'prefix [CHAR] Set or display command prefix.' + print self.prefix + 'transcript [BOOL] Set or display session transcript mode.' + print self.prefix + 'verbose [NUMBER] Set or display verbosity level.' + print self.prefix + 'prompt STRING Change command line prompt.' + print self.prefix + 'history Display command history.' + print self.prefix + 'help Display this help text.' + print self.prefix + 'version Print program version.' + print self.prefix + 'warranty Print copyright statement.' + print self.prefix + 'quit |