├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── Pipfile ├── README.md ├── docs └── api │ └── sonic │ ├── client.html │ └── index.html ├── setup.py └── sonic ├── __init__.py └── client.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 2. 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | ** Versions (please complete the following information):** 22 | - OS: [e.g. Linux] 23 | - Sonic version 24 | - Sonic client version 25 | 26 | 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: Feature Request 5 | labels: fr 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at xmonader@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ahmed T. Youssef 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | gendocs: 2 | pdoc sonic --html --html-dir docs/api --overwrite 3 | 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pdoc3 = "*" 8 | 9 | [packages] 10 | 11 | [requires] 12 | python_version = "3.6" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-sonic-client 2 | 3 | Python client for [sonic](https://github.com/valeriansaliou/sonic) search backend. 4 | 5 | ## Install 6 | 7 | ``` 8 | pip install sonic-client 9 | ``` 10 | 11 | ## Examples 12 | 13 | ### Ingest 14 | 15 | ```python 16 | from sonic import IngestClient 17 | 18 | with IngestClient("127.0.0.1", 1491, "password") as ingestcl: 19 | print(ingestcl.ping()) 20 | print(ingestcl.protocol) 21 | print(ingestcl.bufsize) 22 | ingestcl.push("wiki", "articles", "article-1", "for the love of god hell") 23 | ingestcl.push("wiki", "articles", "article-2", "for the love of satan heaven") 24 | ingestcl.push("wiki", "articles", "article-3", "for the love of lorde hello") 25 | ingestcl.push("wiki", "articles", "article-4", "for the god of loaf helmet") 26 | ``` 27 | 28 | 29 | ### Search 30 | 31 | ```python 32 | from sonic import SearchClient 33 | 34 | with SearchClient("127.0.0.1", 1491, "password") as querycl: 35 | print(querycl.ping()) 36 | print(querycl.query("wiki", "articles", "for")) 37 | print(querycl.query("wiki", "articles", "love")) 38 | print(querycl.suggest("wiki", "articles", "hell")) 39 | ``` 40 | 41 | 42 | ### Control 43 | 44 | ```python 45 | from sonic import ControlClient 46 | 47 | with ControlClient("127.0.0.1", 1491, "password") as controlcl: 48 | print(controlcl.ping()) 49 | controlcl.trigger("consolidate") 50 | ``` 51 | 52 | ## API reference 53 | 54 | API documentation can be found at [docs/api](./docs/api) and also [Browsable](https://xmonader.github.io/python-sonic-client/api/sonic/) 55 | 56 | 57 | ## Difference from asonic 58 | 59 | asonic uses asyncio and this client doesn't. It grew out of needing to use sonic within gevent context 60 | -------------------------------------------------------------------------------- /docs/api/sonic/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sonic.client API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

sonic.client module

21 |
22 |
23 |
24 | Source code 25 |
from enum import Enum
  26 | import socket
  27 | import re
  28 | from queue import Queue
  29 | import itertools
  30 | 
  31 | 
  32 | class SonicServerError(Exception):
  33 |     """Generic Sonic Server exception"""
  34 |     pass
  35 | 
  36 | 
  37 | class ChannelError(Exception):
  38 |     """Sonic Channel specific exception"""
  39 |     pass
  40 | 
  41 | 
  42 | # Commands available on all channels + START that's available on the uninitialized channel
  43 | COMMON_CMDS = [
  44 |     'START',
  45 |     'PING',
  46 |     'HELP',
  47 |     'QUIT'
  48 | ]
  49 | 
  50 | # Channels commands
  51 | ALL_CMDS = {
  52 |     # FIXME: unintialized entry isn't needed anymore.
  53 |     'UNINITIALIZED': [
  54 |         *COMMON_CMDS,
  55 |     ],
  56 |     'ingest': [
  57 |         *COMMON_CMDS,
  58 |         # PUSH <collection> <bucket> <object> "<text>" [LANG(<locale>)]?
  59 |         'PUSH',
  60 |         'POP',     # POP <collection> <bucket> <object> "<text>"
  61 |         'COUNT',   # COUNT <collection> [<bucket> [<object>]?]?
  62 |         'FLUSHC',  # FLUSHC <collection>
  63 |         'FLUSHB',  # FLUSHB <collection> <bucket>
  64 |         'FLUSHO',  # FLUSHO <collection> <bucket> <object>
  65 |     ],
  66 |     'search': [
  67 |         *COMMON_CMDS,
  68 |         # QUERY <collection> <bucket> "<terms>" [LIMIT(<count>)]? [OFFSET(<count>)]? [LANG(<locale>)]?
  69 |         'QUERY',
  70 |         'SUGGEST',  # SUGGEST <collection> <bucket> "<word>" [LIMIT(<count>)]?
  71 |     ],
  72 |     'control': [
  73 |         *COMMON_CMDS,
  74 |         'TRIGGER',  # TRIGGER [<action>]?
  75 |     ]
  76 | }
  77 | 
  78 | # snippet from asonic code.
  79 | 
  80 | 
  81 | def quote_text(text):
  82 |     """Quote text and normalize it in sonic protocol context.
  83 | 
  84 |     Arguments:
  85 |         text str -- text to quote/escape
  86 | 
  87 |     Returns:
  88 |         str -- quoted text
  89 |     """
  90 |     if text is None:
  91 |         return ""
  92 |     return '"' + text.replace('"', '\\"').replace('\r\n', ' ') + '"'
  93 | 
  94 | 
  95 | def is_error(response):
  96 |     """Check if the response is Error or not in sonic context.
  97 | 
  98 |     Errors start with `ERR`
  99 |     Arguments:
 100 |         response {str} -- response string
 101 | 
 102 |     Returns:
 103 |         [bool] -- true if response is an error.
 104 |     """
 105 |     if response.startswith('ERR '):
 106 |         return True
 107 |     return False
 108 | 
 109 | 
 110 | def raise_for_error(response):
 111 |     """Raise SonicServerError in case of error response.
 112 | 
 113 |     Arguments:
 114 |         response {str} -- message to check if it's error or not.
 115 | 
 116 |     Raises:
 117 |         SonicServerError --
 118 | 
 119 |     Returns:
 120 |         str -- the response message
 121 |     """
 122 |     if is_error(response):
 123 |         raise SonicServerError(response)
 124 |     return response
 125 | 
 126 | 
 127 | def _parse_protocol_version(text):
 128 |     """Extracts protocol version from response message
 129 | 
 130 |     Arguments:
 131 |         text {str} -- text that may contain protocol version info (e.g STARTED search protocol(1) buffer(20000) )
 132 | 
 133 |     Raises:
 134 |         ValueError -- Raised when s doesn't have protocol information
 135 | 
 136 |     Returns:
 137 |         str -- protocol version.
 138 |     """
 139 |     matches = re.findall("protocol\((\w+)\)", text)
 140 |     if not matches:
 141 |         raise ValueError("{} doesn't contain protocol(NUMBER)".format(text))
 142 |     return matches[0]
 143 | 
 144 | 
 145 | def _parse_buffer_size(text):
 146 |     """Extracts buffering from response message
 147 | 
 148 |     Arguments:
 149 |         text {str} -- text that may contain buffering info (e.g STARTED search protocol(1) buffer(20000) )
 150 | 
 151 |     Raises:
 152 |         ValueError -- Raised when s doesn't have buffering information
 153 | 
 154 |     Returns:
 155 |         str -- buffering.
 156 |     """
 157 | 
 158 |     matches = re.findall("buffer\((\w+)\)", text)
 159 |     if not matches:
 160 |         raise ValueError("{} doesn't contain buffer(NUMBER)".format(text))
 161 |     return matches[0]
 162 | 
 163 | 
 164 | def _get_async_response_id(text):
 165 |     """Extract async response message id.
 166 | 
 167 |     Arguments:
 168 |         text {str} -- text that may contain async response id (e.g PENDING gn4RLF8M )
 169 | 
 170 |     Raises:
 171 |         ValueError -- [description]
 172 | 
 173 |     Returns:
 174 |         str -- async response id
 175 |     """
 176 |     text = text.strip()
 177 |     matches = re.findall("PENDING (\w+)", text)
 178 |     if not matches:
 179 |         raise ValueError("{} doesn't contain async response id".format(text))
 180 |     return matches[0]
 181 | 
 182 | 
 183 | def pythonify_result(resp):
 184 |     if resp in ["OK", "PONG"]:
 185 |         return True
 186 | 
 187 |     if resp.startswith("EVENT QUERY") or resp.startswith("EVENT SUGGEST"):
 188 |         return resp.split()[3:]
 189 | 
 190 |     if resp.startswith("RESULT"):
 191 |         return int(resp.split()[-1])
 192 |     return resp
 193 | 
 194 | # Channels names
 195 | INGEST = 'ingest'
 196 | SEARCH = 'search'
 197 | CONTROL = 'control'
 198 | 
 199 | class SonicConnection:
 200 |     def __init__(self, host: str, port: int, password: str, channel: str, keepalive: bool=True, timeout: int=60):
 201 |         """Base for sonic connections
 202 | 
 203 |         bufsize: indicates the buffer size to be used while communicating with the server.
 204 |         protocol: sonic protocol version
 205 | 
 206 |         Arguments:
 207 |             host {str} -- sonic server host
 208 |             port {int} -- sonic server port
 209 |             password {str} -- user password defined in `config.cfg` file on the server side.
 210 |             channel {str} -- channel name one of (ingest, search, control)
 211 | 
 212 |         Keyword Arguments:
 213 |             keepalive {bool} -- sets keepalive socket option (default: {True})
 214 |             timeout {int} -- sets socket timeout  (default: {60})
 215 |         """
 216 |         
 217 |         self.host = host
 218 |         self.port = port
 219 |         self._password = password
 220 |         self.channel = channel
 221 |         self.raw = False
 222 |         self.address = self.host, self.port
 223 |         self.keepalive = keepalive
 224 |         self.timeout = timeout
 225 |         self.socket_connect_timeout = 10
 226 |         self.__socket = None
 227 |         self.__reader = None
 228 |         self.__writer = None
 229 |         self.bufize = None
 230 |         self.protocol = None
 231 | 
 232 |     def connect(self):
 233 |         """Connects to sonic server endpoint
 234 | 
 235 |         Returns:
 236 |             bool: True when connection happens and successfully switched to a channel.
 237 |         """
 238 |         resp = self._reader.readline()
 239 |         if 'CONNECTED' in resp:
 240 |             self.connected = True
 241 | 
 242 |         resp = self._execute_command("START", self.channel, self._password)
 243 |         self.protocol = _parse_protocol_version(resp)
 244 |         self.bufsize = _parse_buffer_size(resp)
 245 | 
 246 |         return self.ping()
 247 | 
 248 |     def ping(self):
 249 |         return self._execute_command("PING") == "PONG"
 250 | 
 251 |     def __create_connection(self, address):
 252 |         "Create a TCP socket connection"
 253 |         # we want to mimic what socket.create_connection does to support
 254 |         # ipv4/ipv6, but we want to set options prior to calling
 255 |         # socket.connect()
 256 |         # snippet taken from redis client code.
 257 |         err = None
 258 |         for res in socket.getaddrinfo(self.host, self.port, 0,
 259 |                                       socket.SOCK_STREAM):
 260 |             family, socktype, proto, canonname, socket_address = res
 261 |             sock = None
 262 |             try:
 263 |                 sock = socket.socket(family, socktype, proto)
 264 |                 # TCP_NODELAY
 265 |                 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 266 | 
 267 |                 # TCP_KEEPALIVE
 268 |                 if self.keepalive:
 269 |                     sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
 270 | 
 271 |                 # set the socket_connect_timeout before we connect
 272 |                 if self.socket_connect_timeout:
 273 |                     sock.settimeout(self.timeout)
 274 | 
 275 |                 # connect
 276 |                 sock.connect(socket_address)
 277 | 
 278 |                 # set the socket_timeout now that we're connected
 279 |                 if self.timeout:
 280 |                     sock.settimeout(self.timeout)
 281 |                 return sock
 282 | 
 283 |             except socket.error as _:
 284 |                 err = _
 285 |                 if sock is not None:
 286 |                     sock.close()
 287 | 
 288 |         if err is not None:
 289 |             raise err
 290 |         raise socket.error("socket.getaddrinfo returned an empty list")
 291 | 
 292 |     @property
 293 |     def _socket(self):
 294 |         if not self.__socket:
 295 |             # socket.create_connection(self.address)
 296 |             self.__socket = self.__create_connection(self.address)
 297 | 
 298 |         return self.__socket
 299 | 
 300 |     @property
 301 |     def _reader(self):
 302 |         if not self.__reader:
 303 |             self.__reader = self._socket.makefile('r')
 304 |         return self.__reader
 305 | 
 306 |     @property
 307 |     def _writer(self):
 308 |         if not self.__writer:
 309 |             self.__writer = self._socket.makefile('w')
 310 |         return self.__writer
 311 | 
 312 |     def close(self):
 313 |         """
 314 |         Closes the connection and its resources.
 315 |         """
 316 |         resources = (self.__reader, self.__writer, self.__socket)
 317 |         for rc in resources:
 318 |             if rc is not None:
 319 |                 rc.close()
 320 |         self.__reader = None
 321 |         self.__writer = None
 322 |         self.__socket = None
 323 | 
 324 |     def _format_command(self, cmd, *args):
 325 |         """Format command according to sonic protocol
 326 | 
 327 |         Arguments:
 328 |             cmd {str} -- a valid sonic command
 329 | 
 330 |         Returns:
 331 |             str -- formatted command string to be sent on the wire.
 332 |         """
 333 |         cmd_str = cmd + " "
 334 |         cmd_str += " ".join(args)
 335 |         cmd_str += "\n"  # specs says \n, asonic does \r\n
 336 |         return cmd_str
 337 | 
 338 |     def _execute_command(self, cmd, *args):
 339 |         """Formats and sends command with suitable arguments on the wire to sonic server
 340 | 
 341 |         Arguments:
 342 |             cmd {str} -- valid command
 343 | 
 344 |         Raises:
 345 |             ChannelError -- Raised for unsupported channel commands
 346 | 
 347 |         Returns:
 348 |             object|str -- depends on the `self.raw` mode
 349 |                 if mode is raw: result is always a string
 350 |                 else the result is converted to suitable python response (e.g boolean, int, list)
 351 |         """
 352 |         if cmd not in ALL_CMDS[self.channel]:
 353 |             raise ChannelError(
 354 |                 "command {} isn't allowed in channel {}".format(cmd, self.channel))
 355 | 
 356 |         cmd_str = self._format_command(cmd, *args)
 357 |         self._writer.write(cmd_str)
 358 |         self._writer.flush()
 359 |         resp = self._get_response()
 360 |         return resp
 361 | 
 362 |     def _get_response(self):
 363 |         """Gets a response string from sonic server.
 364 | 
 365 |         Returns:
 366 |             object|str -- depends on the `self.raw` mode
 367 |                 if mode is raw: result is always a string
 368 |                 else the result is converted to suitable python response (e.g boolean, int, list)
 369 |         """
 370 |         resp = raise_for_error(self._reader.readline()).strip()
 371 |         if not self.raw:
 372 |             return pythonify_result(resp)
 373 |         return resp
 374 | 
 375 | 
 376 | class ConnectionPool:
 377 | 
 378 |     def __init__(self, **create_kwargs):
 379 |         """ConnectionPool for Sonic connections.
 380 | 
 381 |         create_kwargs: SonicConnection create kwargs (passed to the connection constructor.)
 382 |         """
 383 |         self._inuse_connections = set()
 384 |         self._available_connections = Queue()
 385 |         self._create_kwargs = create_kwargs
 386 | 
 387 |     def get_connection(self) -> SonicConnection:
 388 |         """Gets a connection from the pool or creates one.
 389 |         
 390 |         Returns:
 391 |             SonicConnection -- Sonic connection.
 392 |         """
 393 |         conn = None
 394 | 
 395 |         if not self._available_connections.empty():
 396 |             conn = self._available_connections.get()
 397 |         else:
 398 |             # make connection and add to active connections
 399 |             conn = self._make_connection()
 400 | 
 401 |         self._inuse_connections.add(conn)
 402 |         return conn
 403 | 
 404 |     def release(self, conn:SonicConnection) -> None:
 405 |         """Releases connection `conn` to the pool
 406 |         
 407 |         Arguments:
 408 |             conn {SonicConnection} -- Connection to release back to the pool.
 409 |         """
 410 |         self._inuse_connections.remove(conn)
 411 |         if conn.ping():
 412 |             self._available_connections.put_nowait(conn)
 413 | 
 414 |     def _make_connection(self) -> SonicConnection:
 415 |         """Creates SonicConnection object and returns it.
 416 |         
 417 |         Returns:
 418 |             SonicConnection -- newly created sonic connection.
 419 |         """
 420 |         con = SonicConnection(**self._create_kwargs)
 421 |         con.connect()
 422 |         return con
 423 | 
 424 |     def close(self) -> None:
 425 |         """Closes the pool and all of the connections.
 426 |         """
 427 |         for con in itertools.chain(self._inuse_connections, self._available_connections):
 428 |             con.close()
 429 | 
 430 | class SonicClient:
 431 | 
 432 |     def __init__(self, host: str, port: int, password: str, channel: str, pool: ConnectionPool=None):
 433 |         """Base for sonic clients
 434 | 
 435 |         bufsize: indicates the buffer size to be used while communicating with the server.
 436 |         protocol: sonic protocol version
 437 | 
 438 |         Arguments:
 439 |             host {str} -- sonic server host
 440 |             port {int} -- sonic server port
 441 |             password {str} -- user password defined in `config.cfg` file on the server side.
 442 |             channel {str} -- channel name one of (ingest, search, control)
 443 | 
 444 |         """
 445 | 
 446 |         self.host = host
 447 |         self.port = port
 448 |         self._password = password
 449 |         self.channel = channel
 450 |         self.bufsize = 0
 451 |         self.protocol = 1
 452 |         self.raw = False
 453 |         self.address = self.host, self.port
 454 | 
 455 |         if not pool:
 456 |             self.pool = ConnectionPool(
 457 |                 host=host, port=port, password=password, channel=channel)
 458 | 
 459 |     def close(self):
 460 |         """close the connection and clean up open resources.
 461 |         """
 462 |         pass
 463 | 
 464 |     def __enter__(self):
 465 |         return self
 466 | 
 467 |     def __exit__(self, exc_type, exc_val, exc_tb):
 468 |         self.close()
 469 | 
 470 |     def get_active_connection(self) -> SonicConnection:
 471 |         """Gets a connection from the pool
 472 |         
 473 |         Returns:
 474 |             SonicConnection -- connection from the pool
 475 |         """
 476 |         active = self.pool.get_connection()
 477 |         active.raw = self.raw
 478 |         return active
 479 | 
 480 |     def _execute_command(self, cmd, *args):
 481 |         """Executes command `cmd` with arguments `args`
 482 |         
 483 |         Arguments:
 484 |             cmd {str} -- command to execute
 485 |             *args     -- `cmd`'s arguments
 486 |         Returns:
 487 |             str|object -- result of execution
 488 |         """
 489 |         active = self.get_active_connection()
 490 |         try:
 491 |             res = active._execute_command(cmd, *args)
 492 |         finally:
 493 |             self.pool.release(active)
 494 |         return res
 495 | 
 496 |     def _execute_command_async(self, cmd, *args):
 497 |         """Executes async command `cmd` with arguments `args` and awaits its result.
 498 |         
 499 |         Arguments:
 500 |             cmd {str} -- command to execute
 501 |             *args     -- `cmd`'s arguments
 502 |         Returns:
 503 |             str|object -- result of execution
 504 |         """
 505 | 
 506 |         active = self.get_active_connection()
 507 |         try:
 508 |             active._execute_command(cmd, *args)
 509 |             resp = active._get_response()
 510 |         finally:
 511 |             self.pool.release(active)
 512 |         return resp
 513 | 
 514 | class CommonCommandsMixin:
 515 |     """Mixin of the commands used by all sonic channels."""
 516 | 
 517 |     def ping(self):
 518 |         """Send ping command to the server
 519 | 
 520 |         Returns:
 521 |             bool -- True if successfully reaching the server.
 522 |         """
 523 |         return self._execute_command("PING")
 524 | 
 525 |     def quit(self):
 526 |         """Quit the channel and closes the connection.
 527 | 
 528 |         """
 529 |         self._execute_command("QUIT")
 530 |         self.close()
 531 | 
 532 |     # TODO: check help.
 533 |     def help(self, *args):
 534 |         """Sends Help query."""
 535 |         return self._execute_command("HELP", *args)
 536 | 
 537 | 
 538 | class IngestClient(SonicClient, CommonCommandsMixin):
 539 |     def __init__(self, host: str, port: str, password: str):
 540 |         super().__init__(host, port, password, INGEST)
 541 | 
 542 |     def push(self, collection: str, bucket: str, object: str, text: str, lang: str=None):
 543 |         """Push search data in the index
 544 | 
 545 |         Arguments:
 546 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 547 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 548 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
 549 |             text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
 550 | 
 551 |         Keyword Arguments:
 552 |             lang {str} -- [description] (default: {None})
 553 | 
 554 |         Returns:
 555 |             bool -- True if search data are pushed in the index.
 556 |         """
 557 | 
 558 |         lang = "LANG({})".format(lang) if lang else ''
 559 |         text = quote_text(text)
 560 |         return self._execute_command("PUSH", collection, bucket, object, text, lang)
 561 | 
 562 |     def pop(self, collection: str, bucket: str, object: str, text: str):
 563 |         """Pop search data from the index
 564 | 
 565 |         Arguments:
 566 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 567 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 568 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
 569 |             text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
 570 | 
 571 |         Returns:
 572 |             int
 573 |         """
 574 |         text = quote_text(text)
 575 |         return self._execute_command("POP", collection, bucket, object, text)
 576 | 
 577 |     def count(self, collection: str, bucket: str=None, object: str=None):
 578 |         """Count indexed search data
 579 | 
 580 |         Arguments:
 581 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 582 | 
 583 |         Keyword Arguments:
 584 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 585 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
 586 | 
 587 |         Returns:
 588 |             int -- count of index search data.
 589 |         """
 590 |         bucket = bucket or ''
 591 |         object = object or ''
 592 |         return self._execute_command('COUNT', collection, bucket, object)
 593 | 
 594 |     def flush_collection(self, collection: str):
 595 |         """Flush all indexed data from a collection
 596 | 
 597 |         Arguments:
 598 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 599 | 
 600 |         Returns:
 601 |             int -- number of flushed data
 602 |         """
 603 |         return self._execute_command('FLUSHC', collection)
 604 | 
 605 |     def flush_bucket(self, collection: str, bucket: str):
 606 |         """Flush all indexed data from a bucket in a collection
 607 | 
 608 |         Arguments:
 609 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 610 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 611 | 
 612 |         Returns:
 613 |             int -- number of flushed data
 614 |         """
 615 |         return self._execute_command('FLUSHB', collection, bucket)
 616 | 
 617 |     def flush_object(self, collection: str, bucket: str, object: str):
 618 |         """Flush all indexed data from an object in a bucket in collection
 619 | 
 620 |         Arguments:
 621 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 622 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 623 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
 624 | 
 625 |         Returns:
 626 |             int -- number of flushed data
 627 |         """
 628 |         return self._execute_command('FLUSHO', collection, bucket, object)
 629 | 
 630 |     def flush(self, collection: str, bucket: str=None, object: str=None):
 631 |         """Flush indexed data in a collection, bucket, or in an object.
 632 | 
 633 |         Arguments:
 634 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
 635 | 
 636 |         Keyword Arguments:
 637 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 638 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
 639 | 
 640 |         Returns:
 641 |             int -- number of flushed data
 642 |         """
 643 |         if not bucket and not object:
 644 |             return self.flush_collection(collection)
 645 |         elif bucket and not object:
 646 |             return self.flush_bucket(collection, bucket)
 647 |         elif object and bucket:
 648 |             return self.flush_object(collection, bucket, object)
 649 | 
 650 | 
 651 | class SearchClient(SonicClient, CommonCommandsMixin):
 652 |     def __init__(self, host: str, port: int, password: str):
 653 |         """Create Sonic client that operates on the Search Channel
 654 | 
 655 |         Arguments:
 656 |             host {str} -- valid reachable host address
 657 |             port {int} -- port number
 658 |             password {str} -- password (defined in config.cfg file on the server side)
 659 | 
 660 |         """
 661 |         super().__init__(host, port, password, SEARCH)
 662 | 
 663 |     def query(self, collection: str, bucket: str, terms: str, limit: int=None, offset: int=None, lang: str=None):
 664 |         """Query the database
 665 | 
 666 |         Arguments:
 667 |             collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
 668 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 669 |             terms {str} --  text for search terms
 670 | 
 671 |         Keyword Arguments:
 672 |             limit {int} -- a positive integer number; set within allowed maximum & minimum limits
 673 |             offset {int} -- a positive integer number; set within allowed maximum & minimum limits
 674 |             lang {str} -- an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if not set, the locale will be guessed from text).
 675 | 
 676 |         Returns:
 677 |             list -- list of objects ids.
 678 |         """
 679 |         limit = "LIMIT({})".format(limit) if limit else ''
 680 |         lang = "LANG({})".format(lang) if lang else ''
 681 |         offset = "OFFSET({})".format(offset) if offset else ''
 682 | 
 683 |         terms = quote_text(terms)
 684 |         return self._execute_command_async(
 685 |             'QUERY', collection, bucket, terms, limit, offset, lang)
 686 | 
 687 |     def suggest(self, collection: str, bucket: str, word: str, limit: int=None):
 688 |         """auto-completes word.
 689 | 
 690 |         Arguments:
 691 |             collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
 692 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
 693 |             word {str} --  word to autocomplete
 694 | 
 695 | 
 696 |         Keyword Arguments:
 697 |             limit {int} -- a positive integer number; set within allowed maximum & minimum limits (default: {None})
 698 | 
 699 |         Returns:
 700 |             list -- list of suggested words.
 701 |         """
 702 |         limit = "LIMIT({})".format(limit) if limit else ''
 703 |         word = quote_text(word)
 704 |         return self._execute_command(
 705 |             'SUGGEST', collection, bucket, word, limit)
 706 | 
 707 | 
 708 | class ControlClient(SonicClient, CommonCommandsMixin):
 709 |     def __init__(self, host: str, port: int, password: str):
 710 |         """Create Sonic client that operates on the Control Channel
 711 | 
 712 |         Arguments:
 713 |             host {str} -- valid reachable host address
 714 |             port {int} -- port number
 715 |             password {str} -- password (defined in config.cfg file on the server side)
 716 | 
 717 |         """
 718 |         super().__init__(host, port, password, CONTROL)
 719 | 
 720 |     def trigger(self, action: str=''):
 721 |         """Trigger an action
 722 | 
 723 |         Keyword Arguments:
 724 |             action {str} --  text for action
 725 |         """
 726 |         self._execute_command('TRIGGER', action)
 727 | 
 728 | 
 729 | def test_ingest():
 730 |     with IngestClient("127.0.0.1", 1491, 'password') as ingestcl:
 731 |         print(ingestcl.ping())
 732 |         print(ingestcl.protocol)
 733 |         print(ingestcl.bufsize)
 734 |         ingestcl.push("wiki", "articles", "article-1",
 735 |                       "for the love of god hell")
 736 |         ingestcl.push("wiki", "articles", "article-2",
 737 |                       "for the love of satan heaven")
 738 |         ingestcl.push("wiki", "articles", "article-3",
 739 |                       "for the love of lorde hello")
 740 |         ingestcl.push("wiki", "articles", "article-4",
 741 |                       "for the god of loaf helmet")
 742 | 
 743 | 
 744 | def test_search():
 745 |     with SearchClient("127.0.0.1", 1491, 'password') as querycl:
 746 |         print(querycl.ping())
 747 |         print(querycl.query("wiki", "articles", "for"))
 748 |         print(querycl.query("wiki", "articles", "love"))
 749 | 
 750 | 
 751 | def test_control():
 752 |     with ControlClient("127.0.0.1", 1491, 'password') as controlcl:
 753 |         print(controlcl.ping())
 754 |         controlcl.trigger("consolidate")
 755 | 
 756 | 
 757 | if __name__ == "__main__":
 758 |     test_ingest()
 759 |     test_search()
 760 |     test_control()
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |

Functions

769 |
770 |
771 | def is_error(response) 772 |
773 |
774 |

Check if the response is Error or not in sonic context.

775 |

Errors start with ERR

776 |

Arguments

777 |

response {str} – response string

778 |

Returns

779 |

[bool] – true if response is an error.

780 |
781 | Source code 782 |
def is_error(response):
 783 |     """Check if the response is Error or not in sonic context.
 784 | 
 785 |     Errors start with `ERR`
 786 |     Arguments:
 787 |         response {str} -- response string
 788 | 
 789 |     Returns:
 790 |         [bool] -- true if response is an error.
 791 |     """
 792 |     if response.startswith('ERR '):
 793 |         return True
 794 |     return False
795 |
796 |
797 |
798 | def pythonify_result(resp) 799 |
800 |
801 |
802 |
803 | Source code 804 |
def pythonify_result(resp):
 805 |     if resp in ["OK", "PONG"]:
 806 |         return True
 807 | 
 808 |     if resp.startswith("EVENT QUERY") or resp.startswith("EVENT SUGGEST"):
 809 |         return resp.split()[3:]
 810 | 
 811 |     if resp.startswith("RESULT"):
 812 |         return int(resp.split()[-1])
 813 |     return resp
814 |
815 |
816 |
817 | def quote_text(text) 818 |
819 |
820 |

Quote text and normalize it in sonic protocol context.

821 |

Arguments

822 |

text str – text to quote/escape

823 |

Returns

824 |

str – quoted text

825 |
826 | Source code 827 |
def quote_text(text):
 828 |     """Quote text and normalize it in sonic protocol context.
 829 | 
 830 |     Arguments:
 831 |         text str -- text to quote/escape
 832 | 
 833 |     Returns:
 834 |         str -- quoted text
 835 |     """
 836 |     if text is None:
 837 |         return ""
 838 |     return '"' + text.replace('"', '\\"').replace('\r\n', ' ') + '"'
839 |
840 |
841 |
842 | def raise_for_error(response) 843 |
844 |
845 |

Raise SonicServerError in case of error response.

846 |

Arguments

847 |

response {str} – message to check if it's error or not.

848 |

Raises

849 |

SonicServerError –

850 |

Returns

851 |

str – the response message

852 |
853 | Source code 854 |
def raise_for_error(response):
 855 |     """Raise SonicServerError in case of error response.
 856 | 
 857 |     Arguments:
 858 |         response {str} -- message to check if it's error or not.
 859 | 
 860 |     Raises:
 861 |         SonicServerError --
 862 | 
 863 |     Returns:
 864 |         str -- the response message
 865 |     """
 866 |     if is_error(response):
 867 |         raise SonicServerError(response)
 868 |     return response
869 |
870 |
871 |
872 | def test_control() 873 |
874 |
875 |
876 |
877 | Source code 878 |
def test_control():
 879 |     with ControlClient("127.0.0.1", 1491, 'password') as controlcl:
 880 |         print(controlcl.ping())
 881 |         controlcl.trigger("consolidate")
882 |
883 |
884 |
885 | def test_ingest() 886 |
887 |
888 |
889 |
890 | Source code 891 |
def test_ingest():
 892 |     with IngestClient("127.0.0.1", 1491, 'password') as ingestcl:
 893 |         print(ingestcl.ping())
 894 |         print(ingestcl.protocol)
 895 |         print(ingestcl.bufsize)
 896 |         ingestcl.push("wiki", "articles", "article-1",
 897 |                       "for the love of god hell")
 898 |         ingestcl.push("wiki", "articles", "article-2",
 899 |                       "for the love of satan heaven")
 900 |         ingestcl.push("wiki", "articles", "article-3",
 901 |                       "for the love of lorde hello")
 902 |         ingestcl.push("wiki", "articles", "article-4",
 903 |                       "for the god of loaf helmet")
904 |
905 |
906 | 909 |
910 |
911 |
912 | Source code 913 |
def test_search():
 914 |     with SearchClient("127.0.0.1", 1491, 'password') as querycl:
 915 |         print(querycl.ping())
 916 |         print(querycl.query("wiki", "articles", "for"))
 917 |         print(querycl.query("wiki", "articles", "love"))
918 |
919 |
920 |
921 |
922 |
923 |

Classes

924 |
925 |
926 | class ChannelError 927 | (ancestors: builtins.Exception, builtins.BaseException) 928 |
929 |
930 |

Sonic Channel specific exception

931 |
932 | Source code 933 |
class ChannelError(Exception):
 934 |     """Sonic Channel specific exception"""
 935 |     pass
936 |
937 |
938 |
939 | class CommonCommandsMixin 940 |
941 |
942 |

Mixin of the commands used by all sonic channels.

943 |
944 | Source code 945 |
class CommonCommandsMixin:
 946 |     """Mixin of the commands used by all sonic channels."""
 947 | 
 948 |     def ping(self):
 949 |         """Send ping command to the server
 950 | 
 951 |         Returns:
 952 |             bool -- True if successfully reaching the server.
 953 |         """
 954 |         return self._execute_command("PING")
 955 | 
 956 |     def quit(self):
 957 |         """Quit the channel and closes the connection.
 958 | 
 959 |         """
 960 |         self._execute_command("QUIT")
 961 |         self.close()
 962 | 
 963 |     # TODO: check help.
 964 |     def help(self, *args):
 965 |         """Sends Help query."""
 966 |         return self._execute_command("HELP", *args)
967 |
968 |

Subclasses

969 | 974 |

Methods

975 |
976 |
977 | def help(self, *args) 978 |
979 |
980 |

Sends Help query.

981 |
982 | Source code 983 |
def help(self, *args):
 984 |     """Sends Help query."""
 985 |     return self._execute_command("HELP", *args)
986 |
987 |
988 |
989 | def ping(self) 990 |
991 |
992 |

Send ping command to the server

993 |

Returns

994 |

bool – True if successfully reaching the server.

995 |
996 | Source code 997 |
def ping(self):
 998 |     """Send ping command to the server
 999 | 
1000 |     Returns:
1001 |         bool -- True if successfully reaching the server.
1002 |     """
1003 |     return self._execute_command("PING")
1004 |
1005 |
1006 |
1007 | def quit(self) 1008 |
1009 |
1010 |

Quit the channel and closes the connection.

1011 |
1012 | Source code 1013 |
def quit(self):
1014 |     """Quit the channel and closes the connection.
1015 | 
1016 |     """
1017 |     self._execute_command("QUIT")
1018 |     self.close()
1019 |
1020 |
1021 |
1022 |
1023 |
1024 | class ConnectionPool 1025 |
1026 |
1027 |
1028 |
1029 | Source code 1030 |
class ConnectionPool:
1031 | 
1032 |     def __init__(self, **create_kwargs):
1033 |         """ConnectionPool for Sonic connections.
1034 | 
1035 |         create_kwargs: SonicConnection create kwargs (passed to the connection constructor.)
1036 |         """
1037 |         self._inuse_connections = set()
1038 |         self._available_connections = Queue()
1039 |         self._create_kwargs = create_kwargs
1040 | 
1041 |     def get_connection(self) -> SonicConnection:
1042 |         """Gets a connection from the pool or creates one.
1043 |         
1044 |         Returns:
1045 |             SonicConnection -- Sonic connection.
1046 |         """
1047 |         conn = None
1048 | 
1049 |         if not self._available_connections.empty():
1050 |             conn = self._available_connections.get()
1051 |         else:
1052 |             # make connection and add to active connections
1053 |             conn = self._make_connection()
1054 | 
1055 |         self._inuse_connections.add(conn)
1056 |         return conn
1057 | 
1058 |     def release(self, conn:SonicConnection) -> None:
1059 |         """Releases connection `conn` to the pool
1060 |         
1061 |         Arguments:
1062 |             conn {SonicConnection} -- Connection to release back to the pool.
1063 |         """
1064 |         self._inuse_connections.remove(conn)
1065 |         if conn.ping():
1066 |             self._available_connections.put_nowait(conn)
1067 | 
1068 |     def _make_connection(self) -> SonicConnection:
1069 |         """Creates SonicConnection object and returns it.
1070 |         
1071 |         Returns:
1072 |             SonicConnection -- newly created sonic connection.
1073 |         """
1074 |         con = SonicConnection(**self._create_kwargs)
1075 |         con.connect()
1076 |         return con
1077 | 
1078 |     def close(self) -> None:
1079 |         """Closes the pool and all of the connections.
1080 |         """
1081 |         for con in itertools.chain(self._inuse_connections, self._available_connections):
1082 |             con.close()
1083 |
1084 |

Methods

1085 |
1086 |
1087 | def __init__(self, **create_kwargs) 1088 |
1089 |
1090 |

ConnectionPool for Sonic connections.

1091 |
1092 |
create_kwargs : SonicConnection create kwargs (passed to the connection constructor.)
1093 |
 
1094 |
1095 |
1096 | Source code 1097 |
def __init__(self, **create_kwargs):
1098 |     """ConnectionPool for Sonic connections.
1099 | 
1100 |     create_kwargs: SonicConnection create kwargs (passed to the connection constructor.)
1101 |     """
1102 |     self._inuse_connections = set()
1103 |     self._available_connections = Queue()
1104 |     self._create_kwargs = create_kwargs
1105 |
1106 |
1107 |
1108 | def close(self) 1109 |
1110 |
1111 |

Closes the pool and all of the connections.

1112 |
1113 | Source code 1114 |
def close(self) -> None:
1115 |     """Closes the pool and all of the connections.
1116 |     """
1117 |     for con in itertools.chain(self._inuse_connections, self._available_connections):
1118 |         con.close()
1119 |
1120 |
1121 |
1122 | def get_connection(self) 1123 |
1124 |
1125 |

Gets a connection from the pool or creates one.

1126 |

Returns

1127 |

SonicConnection – Sonic connection.

1128 |
1129 | Source code 1130 |
def get_connection(self) -> SonicConnection:
1131 |     """Gets a connection from the pool or creates one.
1132 |     
1133 |     Returns:
1134 |         SonicConnection -- Sonic connection.
1135 |     """
1136 |     conn = None
1137 | 
1138 |     if not self._available_connections.empty():
1139 |         conn = self._available_connections.get()
1140 |     else:
1141 |         # make connection and add to active connections
1142 |         conn = self._make_connection()
1143 | 
1144 |     self._inuse_connections.add(conn)
1145 |     return conn
1146 |
1147 |
1148 |
1149 | def release(self, conn) 1150 |
1151 |
1152 |

Releases connection conn to the pool

1153 |

Arguments

1154 |

conn {SonicConnection} – Connection to release back to the pool.

1155 |
1156 | Source code 1157 |
def release(self, conn:SonicConnection) -> None:
1158 |     """Releases connection `conn` to the pool
1159 |     
1160 |     Arguments:
1161 |         conn {SonicConnection} -- Connection to release back to the pool.
1162 |     """
1163 |     self._inuse_connections.remove(conn)
1164 |     if conn.ping():
1165 |         self._available_connections.put_nowait(conn)
1166 |
1167 |
1168 |
1169 |
1170 |
1171 | class ControlClient 1172 | (ancestors: SonicClient, CommonCommandsMixin) 1173 |
1174 |
1175 |

Mixin of the commands used by all sonic channels.

1176 |
1177 | Source code 1178 |
class ControlClient(SonicClient, CommonCommandsMixin):
1179 |     def __init__(self, host: str, port: int, password: str):
1180 |         """Create Sonic client that operates on the Control Channel
1181 | 
1182 |         Arguments:
1183 |             host {str} -- valid reachable host address
1184 |             port {int} -- port number
1185 |             password {str} -- password (defined in config.cfg file on the server side)
1186 | 
1187 |         """
1188 |         super().__init__(host, port, password, CONTROL)
1189 | 
1190 |     def trigger(self, action: str=''):
1191 |         """Trigger an action
1192 | 
1193 |         Keyword Arguments:
1194 |             action {str} --  text for action
1195 |         """
1196 |         self._execute_command('TRIGGER', action)
1197 |
1198 |

Methods

1199 |
1200 |
1201 | def __init__(self, host, port, password) 1202 |
1203 |
1204 |

Create Sonic client that operates on the Control Channel

1205 |

Arguments

1206 |

host {str} – valid reachable host address 1207 | port {int} – port number 1208 | password {str} – password (defined in config.cfg file on the server side)

1209 |
1210 | Source code 1211 |
def __init__(self, host: str, port: int, password: str):
1212 |     """Create Sonic client that operates on the Control Channel
1213 | 
1214 |     Arguments:
1215 |         host {str} -- valid reachable host address
1216 |         port {int} -- port number
1217 |         password {str} -- password (defined in config.cfg file on the server side)
1218 | 
1219 |     """
1220 |     super().__init__(host, port, password, CONTROL)
1221 |
1222 |
1223 |
1224 | def trigger(self, action='') 1225 |
1226 |
1227 |

Trigger an action

1228 |

Keyword Arguments: 1229 | action {str} – 1230 | text for action

1231 |
1232 | Source code 1233 |
def trigger(self, action: str=''):
1234 |     """Trigger an action
1235 | 
1236 |     Keyword Arguments:
1237 |         action {str} --  text for action
1238 |     """
1239 |     self._execute_command('TRIGGER', action)
1240 |
1241 |
1242 |
1243 |

Inherited members

1244 | 1259 |
1260 |
1261 | class IngestClient 1262 | (ancestors: SonicClient, CommonCommandsMixin) 1263 |
1264 |
1265 |

Mixin of the commands used by all sonic channels.

1266 |
1267 | Source code 1268 |
class IngestClient(SonicClient, CommonCommandsMixin):
1269 |     def __init__(self, host: str, port: str, password: str):
1270 |         super().__init__(host, port, password, INGEST)
1271 | 
1272 |     def push(self, collection: str, bucket: str, object: str, text: str, lang: str=None):
1273 |         """Push search data in the index
1274 | 
1275 |         Arguments:
1276 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1277 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1278 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1279 |             text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
1280 | 
1281 |         Keyword Arguments:
1282 |             lang {str} -- [description] (default: {None})
1283 | 
1284 |         Returns:
1285 |             bool -- True if search data are pushed in the index.
1286 |         """
1287 | 
1288 |         lang = "LANG({})".format(lang) if lang else ''
1289 |         text = quote_text(text)
1290 |         return self._execute_command("PUSH", collection, bucket, object, text, lang)
1291 | 
1292 |     def pop(self, collection: str, bucket: str, object: str, text: str):
1293 |         """Pop search data from the index
1294 | 
1295 |         Arguments:
1296 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1297 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1298 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1299 |             text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
1300 | 
1301 |         Returns:
1302 |             int
1303 |         """
1304 |         text = quote_text(text)
1305 |         return self._execute_command("POP", collection, bucket, object, text)
1306 | 
1307 |     def count(self, collection: str, bucket: str=None, object: str=None):
1308 |         """Count indexed search data
1309 | 
1310 |         Arguments:
1311 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1312 | 
1313 |         Keyword Arguments:
1314 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1315 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1316 | 
1317 |         Returns:
1318 |             int -- count of index search data.
1319 |         """
1320 |         bucket = bucket or ''
1321 |         object = object or ''
1322 |         return self._execute_command('COUNT', collection, bucket, object)
1323 | 
1324 |     def flush_collection(self, collection: str):
1325 |         """Flush all indexed data from a collection
1326 | 
1327 |         Arguments:
1328 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1329 | 
1330 |         Returns:
1331 |             int -- number of flushed data
1332 |         """
1333 |         return self._execute_command('FLUSHC', collection)
1334 | 
1335 |     def flush_bucket(self, collection: str, bucket: str):
1336 |         """Flush all indexed data from a bucket in a collection
1337 | 
1338 |         Arguments:
1339 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1340 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1341 | 
1342 |         Returns:
1343 |             int -- number of flushed data
1344 |         """
1345 |         return self._execute_command('FLUSHB', collection, bucket)
1346 | 
1347 |     def flush_object(self, collection: str, bucket: str, object: str):
1348 |         """Flush all indexed data from an object in a bucket in collection
1349 | 
1350 |         Arguments:
1351 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1352 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1353 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1354 | 
1355 |         Returns:
1356 |             int -- number of flushed data
1357 |         """
1358 |         return self._execute_command('FLUSHO', collection, bucket, object)
1359 | 
1360 |     def flush(self, collection: str, bucket: str=None, object: str=None):
1361 |         """Flush indexed data in a collection, bucket, or in an object.
1362 | 
1363 |         Arguments:
1364 |             collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1365 | 
1366 |         Keyword Arguments:
1367 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1368 |             object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1369 | 
1370 |         Returns:
1371 |             int -- number of flushed data
1372 |         """
1373 |         if not bucket and not object:
1374 |             return self.flush_collection(collection)
1375 |         elif bucket and not object:
1376 |             return self.flush_bucket(collection, bucket)
1377 |         elif object and bucket:
1378 |             return self.flush_object(collection, bucket, object)
1379 |
1380 |

Methods

1381 |
1382 |
1383 | def count(self, collection, bucket=None, object=None) 1384 |
1385 |
1386 |

Count indexed search data

1387 |

Arguments

1388 |

collection {str} – 1389 | index collection (ie. what you search in, eg. messages, products, etc.) 1390 | Keyword Arguments: 1391 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1392 | object {str} – 1393 | object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)

1394 |

Returns

1395 |

int – count of index search data.

1396 |
1397 | Source code 1398 |
def count(self, collection: str, bucket: str=None, object: str=None):
1399 |     """Count indexed search data
1400 | 
1401 |     Arguments:
1402 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1403 | 
1404 |     Keyword Arguments:
1405 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1406 |         object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1407 | 
1408 |     Returns:
1409 |         int -- count of index search data.
1410 |     """
1411 |     bucket = bucket or ''
1412 |     object = object or ''
1413 |     return self._execute_command('COUNT', collection, bucket, object)
1414 |
1415 |
1416 |
1417 | def flush(self, collection, bucket=None, object=None) 1418 |
1419 |
1420 |

Flush indexed data in a collection, bucket, or in an object.

1421 |

Arguments

1422 |

collection {str} – 1423 | index collection (ie. what you search in, eg. messages, products, etc.) 1424 | Keyword Arguments: 1425 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1426 | object {str} – 1427 | object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)

1428 |

Returns

1429 |

int – number of flushed data

1430 |
1431 | Source code 1432 |
def flush(self, collection: str, bucket: str=None, object: str=None):
1433 |     """Flush indexed data in a collection, bucket, or in an object.
1434 | 
1435 |     Arguments:
1436 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1437 | 
1438 |     Keyword Arguments:
1439 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1440 |         object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1441 | 
1442 |     Returns:
1443 |         int -- number of flushed data
1444 |     """
1445 |     if not bucket and not object:
1446 |         return self.flush_collection(collection)
1447 |     elif bucket and not object:
1448 |         return self.flush_bucket(collection, bucket)
1449 |     elif object and bucket:
1450 |         return self.flush_object(collection, bucket, object)
1451 |
1452 |
1453 |
1454 | def flush_bucket(self, collection, bucket) 1455 |
1456 |
1457 |

Flush all indexed data from a bucket in a collection

1458 |

Arguments

1459 |

collection {str} – 1460 | index collection (ie. what you search in, eg. messages, products, etc.) 1461 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)

1462 |

Returns

1463 |

int – number of flushed data

1464 |
1465 | Source code 1466 |
def flush_bucket(self, collection: str, bucket: str):
1467 |     """Flush all indexed data from a bucket in a collection
1468 | 
1469 |     Arguments:
1470 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1471 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1472 | 
1473 |     Returns:
1474 |         int -- number of flushed data
1475 |     """
1476 |     return self._execute_command('FLUSHB', collection, bucket)
1477 |
1478 |
1479 |
1480 | def flush_collection(self, collection) 1481 |
1482 |
1483 |

Flush all indexed data from a collection

1484 |

Arguments

1485 |

collection {str} – 1486 | index collection (ie. what you search in, eg. messages, products, etc.)

1487 |

Returns

1488 |

int – number of flushed data

1489 |
1490 | Source code 1491 |
def flush_collection(self, collection: str):
1492 |     """Flush all indexed data from a collection
1493 | 
1494 |     Arguments:
1495 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1496 | 
1497 |     Returns:
1498 |         int -- number of flushed data
1499 |     """
1500 |     return self._execute_command('FLUSHC', collection)
1501 |
1502 |
1503 |
1504 | def flush_object(self, collection, bucket, object) 1505 |
1506 |
1507 |

Flush all indexed data from an object in a bucket in collection

1508 |

Arguments

1509 |

collection {str} – 1510 | index collection (ie. what you search in, eg. messages, products, etc.) 1511 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1512 | object {str} – 1513 | object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)

1514 |

Returns

1515 |

int – number of flushed data

1516 |
1517 | Source code 1518 |
def flush_object(self, collection: str, bucket: str, object: str):
1519 |     """Flush all indexed data from an object in a bucket in collection
1520 | 
1521 |     Arguments:
1522 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1523 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1524 |         object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1525 | 
1526 |     Returns:
1527 |         int -- number of flushed data
1528 |     """
1529 |     return self._execute_command('FLUSHO', collection, bucket, object)
1530 |
1531 |
1532 |
1533 | def pop(self, collection, bucket, object, text) 1534 |
1535 |
1536 |

Pop search data from the index

1537 |

Arguments

1538 |

collection {str} – 1539 | index collection (ie. what you search in, eg. messages, products, etc.) 1540 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1541 | object {str} – 1542 | object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 1543 | text {str} – search text to be indexed can be a single word, or a longer text; within maximum length safety limits

1544 |

Returns

1545 |

int

1546 |
1547 | Source code 1548 |
def pop(self, collection: str, bucket: str, object: str, text: str):
1549 |     """Pop search data from the index
1550 | 
1551 |     Arguments:
1552 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1553 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1554 |         object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1555 |         text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
1556 | 
1557 |     Returns:
1558 |         int
1559 |     """
1560 |     text = quote_text(text)
1561 |     return self._execute_command("POP", collection, bucket, object, text)
1562 |
1563 |
1564 |
1565 | def push(self, collection, bucket, object, text, lang=None) 1566 |
1567 |
1568 |

Push search data in the index

1569 |

Arguments

1570 |

collection {str} – 1571 | index collection (ie. what you search in, eg. messages, products, etc.) 1572 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1573 | object {str} – 1574 | object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 1575 | text {str} – search text to be indexed can be a single word, or a longer text; within maximum length safety limits 1576 | Keyword Arguments: 1577 | lang {str} – [description] (default: {None})

1578 |

Returns

1579 |

bool – True if search data are pushed in the index.

1580 |
1581 | Source code 1582 |
def push(self, collection: str, bucket: str, object: str, text: str, lang: str=None):
1583 |     """Push search data in the index
1584 | 
1585 |     Arguments:
1586 |         collection {str} --  index collection (ie. what you search in, eg. messages, products, etc.)
1587 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1588 |         object {str} --  object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact)
1589 |         text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits
1590 | 
1591 |     Keyword Arguments:
1592 |         lang {str} -- [description] (default: {None})
1593 | 
1594 |     Returns:
1595 |         bool -- True if search data are pushed in the index.
1596 |     """
1597 | 
1598 |     lang = "LANG({})".format(lang) if lang else ''
1599 |     text = quote_text(text)
1600 |     return self._execute_command("PUSH", collection, bucket, object, text, lang)
1601 |
1602 |
1603 |
1604 |

Inherited members

1605 | 1621 |
1622 |
1623 | class SearchClient 1624 | (ancestors: SonicClient, CommonCommandsMixin) 1625 |
1626 |
1627 |

Mixin of the commands used by all sonic channels.

1628 |
1629 | Source code 1630 |
class SearchClient(SonicClient, CommonCommandsMixin):
1631 |     def __init__(self, host: str, port: int, password: str):
1632 |         """Create Sonic client that operates on the Search Channel
1633 | 
1634 |         Arguments:
1635 |             host {str} -- valid reachable host address
1636 |             port {int} -- port number
1637 |             password {str} -- password (defined in config.cfg file on the server side)
1638 | 
1639 |         """
1640 |         super().__init__(host, port, password, SEARCH)
1641 | 
1642 |     def query(self, collection: str, bucket: str, terms: str, limit: int=None, offset: int=None, lang: str=None):
1643 |         """Query the database
1644 | 
1645 |         Arguments:
1646 |             collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
1647 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1648 |             terms {str} --  text for search terms
1649 | 
1650 |         Keyword Arguments:
1651 |             limit {int} -- a positive integer number; set within allowed maximum & minimum limits
1652 |             offset {int} -- a positive integer number; set within allowed maximum & minimum limits
1653 |             lang {str} -- an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if not set, the locale will be guessed from text).
1654 | 
1655 |         Returns:
1656 |             list -- list of objects ids.
1657 |         """
1658 |         limit = "LIMIT({})".format(limit) if limit else ''
1659 |         lang = "LANG({})".format(lang) if lang else ''
1660 |         offset = "OFFSET({})".format(offset) if offset else ''
1661 | 
1662 |         terms = quote_text(terms)
1663 |         return self._execute_command_async(
1664 |             'QUERY', collection, bucket, terms, limit, offset, lang)
1665 | 
1666 |     def suggest(self, collection: str, bucket: str, word: str, limit: int=None):
1667 |         """auto-completes word.
1668 | 
1669 |         Arguments:
1670 |             collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
1671 |             bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1672 |             word {str} --  word to autocomplete
1673 | 
1674 | 
1675 |         Keyword Arguments:
1676 |             limit {int} -- a positive integer number; set within allowed maximum & minimum limits (default: {None})
1677 | 
1678 |         Returns:
1679 |             list -- list of suggested words.
1680 |         """
1681 |         limit = "LIMIT({})".format(limit) if limit else ''
1682 |         word = quote_text(word)
1683 |         return self._execute_command(
1684 |             'SUGGEST', collection, bucket, word, limit)
1685 |
1686 |

Methods

1687 |
1688 |
1689 | def __init__(self, host, port, password) 1690 |
1691 |
1692 |

Create Sonic client that operates on the Search Channel

1693 |

Arguments

1694 |

host {str} – valid reachable host address 1695 | port {int} – port number 1696 | password {str} – password (defined in config.cfg file on the server side)

1697 |
1698 | Source code 1699 |
def __init__(self, host: str, port: int, password: str):
1700 |     """Create Sonic client that operates on the Search Channel
1701 | 
1702 |     Arguments:
1703 |         host {str} -- valid reachable host address
1704 |         port {int} -- port number
1705 |         password {str} -- password (defined in config.cfg file on the server side)
1706 | 
1707 |     """
1708 |     super().__init__(host, port, password, SEARCH)
1709 |
1710 |
1711 |
1712 | def query(self, collection, bucket, terms, limit=None, offset=None, lang=None) 1713 |
1714 |
1715 |

Query the database

1716 |

Arguments

1717 |

collection {str} – index collection (ie. what you search in, eg. messages, products, etc.) 1718 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1719 | terms {str} – 1720 | text for search terms 1721 | Keyword Arguments: 1722 | limit {int} – a positive integer number; set within allowed maximum & minimum limits 1723 | offset {int} – a positive integer number; set within allowed maximum & minimum limits 1724 | lang {str} – an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if not set, the locale will be guessed from text).

1725 |

Returns

1726 |

list – list of objects ids.

1727 |
1728 | Source code 1729 |
def query(self, collection: str, bucket: str, terms: str, limit: int=None, offset: int=None, lang: str=None):
1730 |     """Query the database
1731 | 
1732 |     Arguments:
1733 |         collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
1734 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1735 |         terms {str} --  text for search terms
1736 | 
1737 |     Keyword Arguments:
1738 |         limit {int} -- a positive integer number; set within allowed maximum & minimum limits
1739 |         offset {int} -- a positive integer number; set within allowed maximum & minimum limits
1740 |         lang {str} -- an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if not set, the locale will be guessed from text).
1741 | 
1742 |     Returns:
1743 |         list -- list of objects ids.
1744 |     """
1745 |     limit = "LIMIT({})".format(limit) if limit else ''
1746 |     lang = "LANG({})".format(lang) if lang else ''
1747 |     offset = "OFFSET({})".format(offset) if offset else ''
1748 | 
1749 |     terms = quote_text(terms)
1750 |     return self._execute_command_async(
1751 |         'QUERY', collection, bucket, terms, limit, offset, lang)
1752 |
1753 |
1754 |
1755 | def suggest(self, collection, bucket, word, limit=None) 1756 |
1757 |
1758 |

auto-completes word.

1759 |

Arguments

1760 |

collection {str} – index collection (ie. what you search in, eg. messages, products, etc.) 1761 | bucket {str} – index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 1762 | word {str} – 1763 | word to autocomplete 1764 | Keyword Arguments: 1765 | limit {int} – a positive integer number; set within allowed maximum & minimum limits (default: {None})

1766 |

Returns

1767 |

list – list of suggested words.

1768 |
1769 | Source code 1770 |
def suggest(self, collection: str, bucket: str, word: str, limit: int=None):
1771 |     """auto-completes word.
1772 | 
1773 |     Arguments:
1774 |         collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.)
1775 |         bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..)
1776 |         word {str} --  word to autocomplete
1777 | 
1778 | 
1779 |     Keyword Arguments:
1780 |         limit {int} -- a positive integer number; set within allowed maximum & minimum limits (default: {None})
1781 | 
1782 |     Returns:
1783 |         list -- list of suggested words.
1784 |     """
1785 |     limit = "LIMIT({})".format(limit) if limit else ''
1786 |     word = quote_text(word)
1787 |     return self._execute_command(
1788 |         'SUGGEST', collection, bucket, word, limit)
1789 |
1790 |
1791 |
1792 |

Inherited members

1793 | 1808 |
1809 |
1810 | class SonicClient 1811 |
1812 |
1813 |
1814 |
1815 | Source code 1816 |
class SonicClient:
1817 | 
1818 |     def __init__(self, host: str, port: int, password: str, channel: str, pool: ConnectionPool=None):
1819 |         """Base for sonic clients
1820 | 
1821 |         bufsize: indicates the buffer size to be used while communicating with the server.
1822 |         protocol: sonic protocol version
1823 | 
1824 |         Arguments:
1825 |             host {str} -- sonic server host
1826 |             port {int} -- sonic server port
1827 |             password {str} -- user password defined in `config.cfg` file on the server side.
1828 |             channel {str} -- channel name one of (ingest, search, control)
1829 | 
1830 |         """
1831 | 
1832 |         self.host = host
1833 |         self.port = port
1834 |         self._password = password
1835 |         self.channel = channel
1836 |         self.bufsize = 0
1837 |         self.protocol = 1
1838 |         self.raw = False
1839 |         self.address = self.host, self.port
1840 | 
1841 |         if not pool:
1842 |             self.pool = ConnectionPool(
1843 |                 host=host, port=port, password=password, channel=channel)
1844 | 
1845 |     def close(self):
1846 |         """close the connection and clean up open resources.
1847 |         """
1848 |         pass
1849 | 
1850 |     def __enter__(self):
1851 |         return self
1852 | 
1853 |     def __exit__(self, exc_type, exc_val, exc_tb):
1854 |         self.close()
1855 | 
1856 |     def get_active_connection(self) -> SonicConnection:
1857 |         """Gets a connection from the pool
1858 |         
1859 |         Returns:
1860 |             SonicConnection -- connection from the pool
1861 |         """
1862 |         active = self.pool.get_connection()
1863 |         active.raw = self.raw
1864 |         return active
1865 | 
1866 |     def _execute_command(self, cmd, *args):
1867 |         """Executes command `cmd` with arguments `args`
1868 |         
1869 |         Arguments:
1870 |             cmd {str} -- command to execute
1871 |             *args     -- `cmd`'s arguments
1872 |         Returns:
1873 |             str|object -- result of execution
1874 |         """
1875 |         active = self.get_active_connection()
1876 |         try:
1877 |             res = active._execute_command(cmd, *args)
1878 |         finally:
1879 |             self.pool.release(active)
1880 |         return res
1881 | 
1882 |     def _execute_command_async(self, cmd, *args):
1883 |         """Executes async command `cmd` with arguments `args` and awaits its result.
1884 |         
1885 |         Arguments:
1886 |             cmd {str} -- command to execute
1887 |             *args     -- `cmd`'s arguments
1888 |         Returns:
1889 |             str|object -- result of execution
1890 |         """
1891 | 
1892 |         active = self.get_active_connection()
1893 |         try:
1894 |             active._execute_command(cmd, *args)
1895 |             resp = active._get_response()
1896 |         finally:
1897 |             self.pool.release(active)
1898 |         return resp
1899 |
1900 |

Subclasses

1901 | 1906 |

Methods

1907 |
1908 |
1909 | def __init__(self, host, port, password, channel, pool=None) 1910 |
1911 |
1912 |

Base for sonic clients

1913 |
1914 |
bufsize: indicates the buffer size to be used while communicating with the server.
1915 |
protocol : sonic protocol version
1916 |
 
1917 |
1918 |

Arguments

1919 |

host {str} – sonic server host 1920 | port {int} – sonic server port 1921 | password {str} – user password defined in config.cfg file on the server side. 1922 | channel {str} – channel name one of (ingest, search, control)

1923 |
1924 | Source code 1925 |
def __init__(self, host: str, port: int, password: str, channel: str, pool: ConnectionPool=None):
1926 |     """Base for sonic clients
1927 | 
1928 |     bufsize: indicates the buffer size to be used while communicating with the server.
1929 |     protocol: sonic protocol version
1930 | 
1931 |     Arguments:
1932 |         host {str} -- sonic server host
1933 |         port {int} -- sonic server port
1934 |         password {str} -- user password defined in `config.cfg` file on the server side.
1935 |         channel {str} -- channel name one of (ingest, search, control)
1936 | 
1937 |     """
1938 | 
1939 |     self.host = host
1940 |     self.port = port
1941 |     self._password = password
1942 |     self.channel = channel
1943 |     self.bufsize = 0
1944 |     self.protocol = 1
1945 |     self.raw = False
1946 |     self.address = self.host, self.port
1947 | 
1948 |     if not pool:
1949 |         self.pool = ConnectionPool(
1950 |             host=host, port=port, password=password, channel=channel)
1951 |
1952 |
1953 |
1954 | def close(self) 1955 |
1956 |
1957 |

close the connection and clean up open resources.

1958 |
1959 | Source code 1960 |
def close(self):
1961 |     """close the connection and clean up open resources.
1962 |     """
1963 |     pass
1964 |
1965 |
1966 |
1967 | def get_active_connection(self) 1968 |
1969 |
1970 |

Gets a connection from the pool

1971 |

Returns

1972 |

SonicConnection – connection from the pool

1973 |
1974 | Source code 1975 |
def get_active_connection(self) -> SonicConnection:
1976 |     """Gets a connection from the pool
1977 |     
1978 |     Returns:
1979 |         SonicConnection -- connection from the pool
1980 |     """
1981 |     active = self.pool.get_connection()
1982 |     active.raw = self.raw
1983 |     return active
1984 |
1985 |
1986 |
1987 |
1988 |
1989 | class SonicConnection 1990 |
1991 |
1992 |
1993 |
1994 | Source code 1995 |
class SonicConnection:
1996 |     def __init__(self, host: str, port: int, password: str, channel: str, keepalive: bool=True, timeout: int=60):
1997 |         """Base for sonic connections
1998 | 
1999 |         bufsize: indicates the buffer size to be used while communicating with the server.
2000 |         protocol: sonic protocol version
2001 | 
2002 |         Arguments:
2003 |             host {str} -- sonic server host
2004 |             port {int} -- sonic server port
2005 |             password {str} -- user password defined in `config.cfg` file on the server side.
2006 |             channel {str} -- channel name one of (ingest, search, control)
2007 | 
2008 |         Keyword Arguments:
2009 |             keepalive {bool} -- sets keepalive socket option (default: {True})
2010 |             timeout {int} -- sets socket timeout  (default: {60})
2011 |         """
2012 |         
2013 |         self.host = host
2014 |         self.port = port
2015 |         self._password = password
2016 |         self.channel = channel
2017 |         self.raw = False
2018 |         self.address = self.host, self.port
2019 |         self.keepalive = keepalive
2020 |         self.timeout = timeout
2021 |         self.socket_connect_timeout = 10
2022 |         self.__socket = None
2023 |         self.__reader = None
2024 |         self.__writer = None
2025 |         self.bufize = None
2026 |         self.protocol = None
2027 | 
2028 |     def connect(self):
2029 |         """Connects to sonic server endpoint
2030 | 
2031 |         Returns:
2032 |             bool: True when connection happens and successfully switched to a channel.
2033 |         """
2034 |         resp = self._reader.readline()
2035 |         if 'CONNECTED' in resp:
2036 |             self.connected = True
2037 | 
2038 |         resp = self._execute_command("START", self.channel, self._password)
2039 |         self.protocol = _parse_protocol_version(resp)
2040 |         self.bufsize = _parse_buffer_size(resp)
2041 | 
2042 |         return self.ping()
2043 | 
2044 |     def ping(self):
2045 |         return self._execute_command("PING") == "PONG"
2046 | 
2047 |     def __create_connection(self, address):
2048 |         "Create a TCP socket connection"
2049 |         # we want to mimic what socket.create_connection does to support
2050 |         # ipv4/ipv6, but we want to set options prior to calling
2051 |         # socket.connect()
2052 |         # snippet taken from redis client code.
2053 |         err = None
2054 |         for res in socket.getaddrinfo(self.host, self.port, 0,
2055 |                                       socket.SOCK_STREAM):
2056 |             family, socktype, proto, canonname, socket_address = res
2057 |             sock = None
2058 |             try:
2059 |                 sock = socket.socket(family, socktype, proto)
2060 |                 # TCP_NODELAY
2061 |                 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
2062 | 
2063 |                 # TCP_KEEPALIVE
2064 |                 if self.keepalive:
2065 |                     sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
2066 | 
2067 |                 # set the socket_connect_timeout before we connect
2068 |                 if self.socket_connect_timeout:
2069 |                     sock.settimeout(self.timeout)
2070 | 
2071 |                 # connect
2072 |                 sock.connect(socket_address)
2073 | 
2074 |                 # set the socket_timeout now that we're connected
2075 |                 if self.timeout:
2076 |                     sock.settimeout(self.timeout)
2077 |                 return sock
2078 | 
2079 |             except socket.error as _:
2080 |                 err = _
2081 |                 if sock is not None:
2082 |                     sock.close()
2083 | 
2084 |         if err is not None:
2085 |             raise err
2086 |         raise socket.error("socket.getaddrinfo returned an empty list")
2087 | 
2088 |     @property
2089 |     def _socket(self):
2090 |         if not self.__socket:
2091 |             # socket.create_connection(self.address)
2092 |             self.__socket = self.__create_connection(self.address)
2093 | 
2094 |         return self.__socket
2095 | 
2096 |     @property
2097 |     def _reader(self):
2098 |         if not self.__reader:
2099 |             self.__reader = self._socket.makefile('r')
2100 |         return self.__reader
2101 | 
2102 |     @property
2103 |     def _writer(self):
2104 |         if not self.__writer:
2105 |             self.__writer = self._socket.makefile('w')
2106 |         return self.__writer
2107 | 
2108 |     def close(self):
2109 |         """
2110 |         Closes the connection and its resources.
2111 |         """
2112 |         resources = (self.__reader, self.__writer, self.__socket)
2113 |         for rc in resources:
2114 |             if rc is not None:
2115 |                 rc.close()
2116 |         self.__reader = None
2117 |         self.__writer = None
2118 |         self.__socket = None
2119 | 
2120 |     def _format_command(self, cmd, *args):
2121 |         """Format command according to sonic protocol
2122 | 
2123 |         Arguments:
2124 |             cmd {str} -- a valid sonic command
2125 | 
2126 |         Returns:
2127 |             str -- formatted command string to be sent on the wire.
2128 |         """
2129 |         cmd_str = cmd + " "
2130 |         cmd_str += " ".join(args)
2131 |         cmd_str += "\n"  # specs says \n, asonic does \r\n
2132 |         return cmd_str
2133 | 
2134 |     def _execute_command(self, cmd, *args):
2135 |         """Formats and sends command with suitable arguments on the wire to sonic server
2136 | 
2137 |         Arguments:
2138 |             cmd {str} -- valid command
2139 | 
2140 |         Raises:
2141 |             ChannelError -- Raised for unsupported channel commands
2142 | 
2143 |         Returns:
2144 |             object|str -- depends on the `self.raw` mode
2145 |                 if mode is raw: result is always a string
2146 |                 else the result is converted to suitable python response (e.g boolean, int, list)
2147 |         """
2148 |         if cmd not in ALL_CMDS[self.channel]:
2149 |             raise ChannelError(
2150 |                 "command {} isn't allowed in channel {}".format(cmd, self.channel))
2151 | 
2152 |         cmd_str = self._format_command(cmd, *args)
2153 |         self._writer.write(cmd_str)
2154 |         self._writer.flush()
2155 |         resp = self._get_response()
2156 |         return resp
2157 | 
2158 |     def _get_response(self):
2159 |         """Gets a response string from sonic server.
2160 | 
2161 |         Returns:
2162 |             object|str -- depends on the `self.raw` mode
2163 |                 if mode is raw: result is always a string
2164 |                 else the result is converted to suitable python response (e.g boolean, int, list)
2165 |         """
2166 |         resp = raise_for_error(self._reader.readline()).strip()
2167 |         if not self.raw:
2168 |             return pythonify_result(resp)
2169 |         return resp
2170 |
2171 |

Methods

2172 |
2173 |
2174 | def __init__(self, host, port, password, channel, keepalive=True, timeout=60) 2175 |
2176 |
2177 |

Base for sonic connections

2178 |
2179 |
bufsize: indicates the buffer size to be used while communicating with the server.
2180 |
protocol : sonic protocol version
2181 |
 
2182 |
2183 |

Arguments

2184 |

host {str} – sonic server host 2185 | port {int} – sonic server port 2186 | password {str} – user password defined in config.cfg file on the server side. 2187 | channel {str} – channel name one of (ingest, search, control) 2188 | Keyword Arguments: 2189 | keepalive {bool} – sets keepalive socket option (default: {True}) 2190 | timeout {int} – sets socket timeout 2191 | (default: {60})

2192 |
2193 | Source code 2194 |
def __init__(self, host: str, port: int, password: str, channel: str, keepalive: bool=True, timeout: int=60):
2195 |     """Base for sonic connections
2196 | 
2197 |     bufsize: indicates the buffer size to be used while communicating with the server.
2198 |     protocol: sonic protocol version
2199 | 
2200 |     Arguments:
2201 |         host {str} -- sonic server host
2202 |         port {int} -- sonic server port
2203 |         password {str} -- user password defined in `config.cfg` file on the server side.
2204 |         channel {str} -- channel name one of (ingest, search, control)
2205 | 
2206 |     Keyword Arguments:
2207 |         keepalive {bool} -- sets keepalive socket option (default: {True})
2208 |         timeout {int} -- sets socket timeout  (default: {60})
2209 |     """
2210 |     
2211 |     self.host = host
2212 |     self.port = port
2213 |     self._password = password
2214 |     self.channel = channel
2215 |     self.raw = False
2216 |     self.address = self.host, self.port
2217 |     self.keepalive = keepalive
2218 |     self.timeout = timeout
2219 |     self.socket_connect_timeout = 10
2220 |     self.__socket = None
2221 |     self.__reader = None
2222 |     self.__writer = None
2223 |     self.bufize = None
2224 |     self.protocol = None
2225 |
2226 |
2227 |
2228 | def close(self) 2229 |
2230 |
2231 |

Closes the connection and its resources.

2232 |
2233 | Source code 2234 |
def close(self):
2235 |     """
2236 |     Closes the connection and its resources.
2237 |     """
2238 |     resources = (self.__reader, self.__writer, self.__socket)
2239 |     for rc in resources:
2240 |         if rc is not None:
2241 |             rc.close()
2242 |     self.__reader = None
2243 |     self.__writer = None
2244 |     self.__socket = None
2245 |
2246 |
2247 |
2248 | def connect(self) 2249 |
2250 |
2251 |

Connects to sonic server endpoint

2252 |

Returns

2253 |
2254 |
bool
2255 |
True when connection happens and successfully switched to a channel.
2256 |
2257 |
2258 | Source code 2259 |
def connect(self):
2260 |     """Connects to sonic server endpoint
2261 | 
2262 |     Returns:
2263 |         bool: True when connection happens and successfully switched to a channel.
2264 |     """
2265 |     resp = self._reader.readline()
2266 |     if 'CONNECTED' in resp:
2267 |         self.connected = True
2268 | 
2269 |     resp = self._execute_command("START", self.channel, self._password)
2270 |     self.protocol = _parse_protocol_version(resp)
2271 |     self.bufsize = _parse_buffer_size(resp)
2272 | 
2273 |     return self.ping()
2274 |
2275 |
2276 |
2277 | def ping(self) 2278 |
2279 |
2280 |
2281 |
2282 | Source code 2283 |
def ping(self):
2284 |     return self._execute_command("PING") == "PONG"
2285 |
2286 |
2287 |
2288 |
2289 |
2290 | class SonicServerError 2291 | (ancestors: builtins.Exception, builtins.BaseException) 2292 |
2293 |
2294 |

Generic Sonic Server exception

2295 |
2296 | Source code 2297 |
class SonicServerError(Exception):
2298 |     """Generic Sonic Server exception"""
2299 |     pass
2300 |
2301 |
2302 |
2303 |
2304 |
2305 | 2400 |
2401 | 2404 | 2405 | 2406 | 2407 | -------------------------------------------------------------------------------- /docs/api/sonic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sonic API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

sonic module

21 |
22 |
23 |
24 | Source code 25 |
from .client import IngestClient, SearchClient, ControlClient
26 | from .client import ALL_CMDS as CHANNELS_CMDS
27 |
28 |
29 |
30 |

Sub-modules

31 |
32 |
sonic.client
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 58 |
59 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | # can't have the entry_points option here. 5 | from distutils.core import setup 6 | 7 | with open('README.md') as f: 8 | long_description = f.read() 9 | 10 | setup(name='sonic-client', 11 | version='0.0.5', 12 | author="Ahmed T. Youssef", 13 | author_email="xmonader@gmail.com", 14 | description='python client for sonic search backend', 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | packages=['sonic'], 18 | url="https://github.com/xmonader/python-sonic-client", 19 | license='BSD 3-Clause License', 20 | classifiers=[ 21 | 'Development Status :: 3 - Alpha', 22 | 'Environment :: Console', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: Apache Software License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /sonic/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import IngestClient, SearchClient, ControlClient 2 | from .client import ALL_CMDS as CHANNELS_CMDS 3 | -------------------------------------------------------------------------------- /sonic/client.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import socket 3 | import re 4 | from queue import Queue 5 | import itertools 6 | 7 | 8 | class SonicServerError(Exception): 9 | """Generic Sonic Server exception""" 10 | pass 11 | 12 | 13 | class ChannelError(Exception): 14 | """Sonic Channel specific exception""" 15 | pass 16 | 17 | 18 | # Commands available on all channels + START that's available on the uninitialized channel 19 | COMMON_CMDS = [ 20 | 'START', 21 | 'PING', 22 | 'HELP', 23 | 'QUIT' 24 | ] 25 | 26 | # Channels commands 27 | ALL_CMDS = { 28 | # FIXME: unintialized entry isn't needed anymore. 29 | 'UNINITIALIZED': [ 30 | *COMMON_CMDS, 31 | ], 32 | 'ingest': [ 33 | *COMMON_CMDS, 34 | # PUSH "" [LANG()]? 35 | 'PUSH', 36 | 'POP', # POP "" 37 | 'COUNT', # COUNT [ []?]? 38 | 'FLUSHC', # FLUSHC 39 | 'FLUSHB', # FLUSHB 40 | 'FLUSHO', # FLUSHO 41 | ], 42 | 'search': [ 43 | *COMMON_CMDS, 44 | # QUERY "" [LIMIT()]? [OFFSET()]? [LANG()]? 45 | 'QUERY', 46 | 'SUGGEST', # SUGGEST "" [LIMIT()]? 47 | ], 48 | 'control': [ 49 | *COMMON_CMDS, 50 | 'TRIGGER', # TRIGGER []? 51 | ] 52 | } 53 | 54 | # snippet from asonic code. 55 | 56 | 57 | def quote_text(text): 58 | """Quote text and normalize it in sonic protocol context. 59 | 60 | Arguments: 61 | text str -- text to quote/escape 62 | 63 | Returns: 64 | str -- quoted text 65 | """ 66 | if text is None: 67 | return "" 68 | return '"' + text.replace('"', '\\"').replace('\r\n', ' ').replace('\n', ' ') + '"' 69 | 70 | 71 | def is_error(response): 72 | """Check if the response is Error or not in sonic context. 73 | 74 | Errors start with `ERR` 75 | Arguments: 76 | response {str} -- response string 77 | 78 | Returns: 79 | [bool] -- true if response is an error. 80 | """ 81 | if response.startswith('ERR '): 82 | return True 83 | return False 84 | 85 | 86 | def raise_for_error(response): 87 | """Raise SonicServerError in case of error response. 88 | 89 | Arguments: 90 | response {str} -- message to check if it's error or not. 91 | 92 | Raises: 93 | SonicServerError -- 94 | 95 | Returns: 96 | str -- the response message 97 | """ 98 | if is_error(response): 99 | raise SonicServerError(response) 100 | return response 101 | 102 | 103 | def _parse_protocol_version(text): 104 | """Extracts protocol version from response message 105 | 106 | Arguments: 107 | text {str} -- text that may contain protocol version info (e.g STARTED search protocol(1) buffer(20000) ) 108 | 109 | Raises: 110 | ValueError -- Raised when s doesn't have protocol information 111 | 112 | Returns: 113 | str -- protocol version. 114 | """ 115 | matches = re.findall("protocol\((\w+)\)", text) 116 | if not matches: 117 | raise ValueError("{} doesn't contain protocol(NUMBER)".format(text)) 118 | return matches[0] 119 | 120 | 121 | def _parse_buffer_size(text): 122 | """Extracts buffering from response message 123 | 124 | Arguments: 125 | text {str} -- text that may contain buffering info (e.g STARTED search protocol(1) buffer(20000) ) 126 | 127 | Raises: 128 | ValueError -- Raised when s doesn't have buffering information 129 | 130 | Returns: 131 | str -- buffering. 132 | """ 133 | 134 | matches = re.findall("buffer\((\w+)\)", text) 135 | if not matches: 136 | raise ValueError("{} doesn't contain buffer(NUMBER)".format(text)) 137 | return matches[0] 138 | 139 | 140 | def _get_async_response_id(text): 141 | """Extract async response message id. 142 | 143 | Arguments: 144 | text {str} -- text that may contain async response id (e.g PENDING gn4RLF8M ) 145 | 146 | Raises: 147 | ValueError -- [description] 148 | 149 | Returns: 150 | str -- async response id 151 | """ 152 | text = text.strip() 153 | matches = re.findall("PENDING (\w+)", text) 154 | if not matches: 155 | raise ValueError("{} doesn't contain async response id".format(text)) 156 | return matches[0] 157 | 158 | 159 | def pythonify_result(resp): 160 | if resp in ["OK", "PONG"]: 161 | return True 162 | 163 | if resp.startswith("EVENT QUERY") or resp.startswith("EVENT SUGGEST"): 164 | return resp.split()[3:] 165 | 166 | if resp.startswith("RESULT"): 167 | return int(resp.split()[-1]) 168 | return resp 169 | 170 | # Channels names 171 | INGEST = 'ingest' 172 | SEARCH = 'search' 173 | CONTROL = 'control' 174 | 175 | class SonicConnection: 176 | def __init__(self, host: str, port: int, password: str, channel: str, keepalive: bool=True, timeout: int=60): 177 | """Base for sonic connections 178 | 179 | bufsize: indicates the buffer size to be used while communicating with the server. 180 | protocol: sonic protocol version 181 | 182 | Arguments: 183 | host {str} -- sonic server host 184 | port {int} -- sonic server port 185 | password {str} -- user password defined in `config.cfg` file on the server side. 186 | channel {str} -- channel name one of (ingest, search, control) 187 | 188 | Keyword Arguments: 189 | keepalive {bool} -- sets keepalive socket option (default: {True}) 190 | timeout {int} -- sets socket timeout (default: {60}) 191 | """ 192 | 193 | self.host = host 194 | self.port = port 195 | self._password = password 196 | self.channel = channel 197 | self.raw = False 198 | self.address = self.host, self.port 199 | self.keepalive = keepalive 200 | self.timeout = timeout 201 | self.socket_connect_timeout = 10 202 | self.__socket = None 203 | self.__reader = None 204 | self.__writer = None 205 | self.bufize = None 206 | self.protocol = None 207 | 208 | def connect(self): 209 | """Connects to sonic server endpoint 210 | 211 | Returns: 212 | bool: True when connection happens and successfully switched to a channel. 213 | """ 214 | resp = self._reader.readline() 215 | if 'CONNECTED' in resp: 216 | self.connected = True 217 | 218 | resp = self._execute_command("START", self.channel, self._password) 219 | self.protocol = _parse_protocol_version(resp) 220 | self.bufsize = _parse_buffer_size(resp) 221 | 222 | return self.ping() 223 | 224 | def ping(self): 225 | res = self._execute_command("PING") 226 | return res == "PONG" if self.raw else res 227 | 228 | def __create_connection(self, address): 229 | "Create a TCP socket connection" 230 | # we want to mimic what socket.create_connection does to support 231 | # ipv4/ipv6, but we want to set options prior to calling 232 | # socket.connect() 233 | # snippet taken from redis client code. 234 | err = None 235 | for res in socket.getaddrinfo(self.host, self.port, 0, 236 | socket.SOCK_STREAM): 237 | family, socktype, proto, canonname, socket_address = res 238 | sock = None 239 | try: 240 | sock = socket.socket(family, socktype, proto) 241 | # TCP_NODELAY 242 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 243 | 244 | # TCP_KEEPALIVE 245 | if self.keepalive: 246 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 247 | 248 | # set the socket_connect_timeout before we connect 249 | if self.socket_connect_timeout: 250 | sock.settimeout(self.timeout) 251 | 252 | # connect 253 | sock.connect(socket_address) 254 | 255 | # set the socket_timeout now that we're connected 256 | if self.timeout: 257 | sock.settimeout(self.timeout) 258 | return sock 259 | 260 | except socket.error as _: 261 | err = _ 262 | if sock is not None: 263 | sock.close() 264 | 265 | if err is not None: 266 | raise err 267 | raise socket.error("socket.getaddrinfo returned an empty list") 268 | 269 | @property 270 | def _socket(self): 271 | if not self.__socket: 272 | # socket.create_connection(self.address) 273 | self.__socket = self.__create_connection(self.address) 274 | 275 | return self.__socket 276 | 277 | @property 278 | def _reader(self): 279 | if not self.__reader: 280 | self.__reader = self._socket.makefile('r', encoding='utf-8') 281 | return self.__reader 282 | 283 | @property 284 | def _writer(self): 285 | if not self.__writer: 286 | self.__writer = self._socket.makefile('w', encoding='utf-8') 287 | return self.__writer 288 | 289 | def close(self): 290 | """ 291 | Closes the connection and its resources. 292 | """ 293 | resources = (self.__reader, self.__writer, self.__socket) 294 | for rc in resources: 295 | if rc is not None: 296 | rc.close() 297 | self.__reader = None 298 | self.__writer = None 299 | self.__socket = None 300 | 301 | def _format_command(self, cmd, *args): 302 | """Format command according to sonic protocol 303 | 304 | Arguments: 305 | cmd {str} -- a valid sonic command 306 | 307 | Returns: 308 | str -- formatted command string to be sent on the wire. 309 | """ 310 | cmd_str = cmd + " " 311 | cmd_str += " ".join(args) 312 | cmd_str += "\n" # specs says \n, asonic does \r\n 313 | return cmd_str 314 | 315 | def _execute_command(self, cmd, *args): 316 | """Formats and sends command with suitable arguments on the wire to sonic server 317 | 318 | Arguments: 319 | cmd {str} -- valid command 320 | 321 | Raises: 322 | ChannelError -- Raised for unsupported channel commands 323 | 324 | Returns: 325 | object|str -- depends on the `self.raw` mode 326 | if mode is raw: result is always a string 327 | else the result is converted to suitable python response (e.g boolean, int, list) 328 | """ 329 | if cmd not in ALL_CMDS[self.channel]: 330 | raise ChannelError( 331 | "command {} isn't allowed in channel {}".format(cmd, self.channel)) 332 | 333 | cmd_str = self._format_command(cmd, *args) 334 | self._writer.write(cmd_str) 335 | self._writer.flush() 336 | resp = self._get_response() 337 | return resp 338 | 339 | def _get_response(self): 340 | """Gets a response string from sonic server. 341 | 342 | Returns: 343 | object|str -- depends on the `self.raw` mode 344 | if mode is raw: result is always a string 345 | else the result is converted to suitable python response (e.g boolean, int, list) 346 | """ 347 | resp = raise_for_error(self._reader.readline()).strip() 348 | if not self.raw: 349 | return pythonify_result(resp) 350 | return resp 351 | 352 | 353 | class ConnectionPool: 354 | 355 | def __init__(self, **create_kwargs): 356 | """ConnectionPool for Sonic connections. 357 | 358 | create_kwargs: SonicConnection create kwargs (passed to the connection constructor.) 359 | """ 360 | self._inuse_connections = set() 361 | self._available_connections = Queue() 362 | self._create_kwargs = create_kwargs 363 | 364 | def get_connection(self) -> SonicConnection: 365 | """Gets a connection from the pool or creates one. 366 | 367 | Returns: 368 | SonicConnection -- Sonic connection. 369 | """ 370 | conn = None 371 | 372 | if not self._available_connections.empty(): 373 | conn = self._available_connections.get() 374 | else: 375 | # make connection and add to active connections 376 | conn = self._make_connection() 377 | 378 | self._inuse_connections.add(conn) 379 | return conn 380 | 381 | def release(self, conn:SonicConnection) -> None: 382 | """Releases connection `conn` to the pool 383 | 384 | Arguments: 385 | conn {SonicConnection} -- Connection to release back to the pool. 386 | """ 387 | self._inuse_connections.remove(conn) 388 | if conn.ping(): 389 | self._available_connections.put_nowait(conn) 390 | 391 | def _make_connection(self) -> SonicConnection: 392 | """Creates SonicConnection object and returns it. 393 | 394 | Returns: 395 | SonicConnection -- newly created sonic connection. 396 | """ 397 | con = SonicConnection(**self._create_kwargs) 398 | con.connect() 399 | return con 400 | 401 | def close(self) -> None: 402 | """Closes the pool and all of the connections. 403 | """ 404 | for con in itertools.chain(self._inuse_connections, self._available_connections): 405 | con.close() 406 | 407 | class SonicClient: 408 | 409 | def __init__(self, host: str, port: int, password: str, channel: str, pool: ConnectionPool=None): 410 | """Base for sonic clients 411 | 412 | bufsize: indicates the buffer size to be used while communicating with the server. 413 | protocol: sonic protocol version 414 | 415 | Arguments: 416 | host {str} -- sonic server host 417 | port {int} -- sonic server port 418 | password {str} -- user password defined in `config.cfg` file on the server side. 419 | channel {str} -- channel name one of (ingest, search, control) 420 | 421 | """ 422 | 423 | self.host = host 424 | self.port = port 425 | self._password = password 426 | self.channel = channel 427 | self.bufsize = 0 428 | self.protocol = 1 429 | self.raw = False 430 | self.address = self.host, self.port 431 | 432 | if not pool: 433 | self.pool = ConnectionPool( 434 | host=host, port=port, password=password, channel=channel) 435 | 436 | def close(self): 437 | """close the connection and clean up open resources. 438 | """ 439 | pass 440 | 441 | def __enter__(self): 442 | return self 443 | 444 | def __exit__(self, exc_type, exc_val, exc_tb): 445 | self.close() 446 | 447 | def get_active_connection(self) -> SonicConnection: 448 | """Gets a connection from the pool 449 | 450 | Returns: 451 | SonicConnection -- connection from the pool 452 | """ 453 | active = self.pool.get_connection() 454 | active.raw = self.raw 455 | return active 456 | 457 | def _execute_command(self, cmd, *args): 458 | """Executes command `cmd` with arguments `args` 459 | 460 | Arguments: 461 | cmd {str} -- command to execute 462 | *args -- `cmd`'s arguments 463 | Returns: 464 | str|object -- result of execution 465 | """ 466 | active = self.get_active_connection() 467 | try: 468 | res = active._execute_command(cmd, *args) 469 | finally: 470 | self.pool.release(active) 471 | return res 472 | 473 | def _execute_command_async(self, cmd, *args): 474 | """Executes async command `cmd` with arguments `args` and awaits its result. 475 | 476 | Arguments: 477 | cmd {str} -- command to execute 478 | *args -- `cmd`'s arguments 479 | Returns: 480 | str|object -- result of execution 481 | """ 482 | 483 | active = self.get_active_connection() 484 | try: 485 | active._execute_command(cmd, *args) 486 | resp = active._get_response() 487 | finally: 488 | self.pool.release(active) 489 | return resp 490 | 491 | class CommonCommandsMixin: 492 | """Mixin of the commands used by all sonic channels.""" 493 | 494 | def ping(self): 495 | """Send ping command to the server 496 | 497 | Returns: 498 | bool -- True if successfully reaching the server. 499 | """ 500 | return self._execute_command("PING") 501 | 502 | def quit(self): 503 | """Quit the channel and closes the connection. 504 | 505 | """ 506 | self._execute_command("QUIT") 507 | self.close() 508 | 509 | # TODO: check help. 510 | def help(self, *args): 511 | """Sends Help query.""" 512 | return self._execute_command("HELP", *args) 513 | 514 | 515 | class IngestClient(SonicClient, CommonCommandsMixin): 516 | def __init__(self, host: str, port: str, password: str): 517 | super().__init__(host, port, password, INGEST) 518 | 519 | def push(self, collection: str, bucket: str, object: str, text: str, lang: str=None): 520 | """Push search data in the index 521 | 522 | Arguments: 523 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 524 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 525 | object {str} -- object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 526 | text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits 527 | 528 | Keyword Arguments: 529 | lang {str} -- [description] (default: {None}) 530 | 531 | Returns: 532 | bool -- True if search data are pushed in the index. 533 | """ 534 | 535 | lang = "LANG({})".format(lang) if lang else '' 536 | text = quote_text(text) 537 | return self._execute_command("PUSH", collection, bucket, object, text, lang) 538 | 539 | def pop(self, collection: str, bucket: str, object: str, text: str): 540 | """Pop search data from the index 541 | 542 | Arguments: 543 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 544 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 545 | object {str} -- object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 546 | text {str} -- search text to be indexed can be a single word, or a longer text; within maximum length safety limits 547 | 548 | Returns: 549 | int 550 | """ 551 | text = quote_text(text) 552 | return self._execute_command("POP", collection, bucket, object, text) 553 | 554 | def count(self, collection: str, bucket: str=None, object: str=None): 555 | """Count indexed search data 556 | 557 | Arguments: 558 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 559 | 560 | Keyword Arguments: 561 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 562 | object {str} -- object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 563 | 564 | Returns: 565 | int -- count of index search data. 566 | """ 567 | bucket = bucket or '' 568 | object = object or '' 569 | return self._execute_command('COUNT', collection, bucket, object) 570 | 571 | def flush_collection(self, collection: str): 572 | """Flush all indexed data from a collection 573 | 574 | Arguments: 575 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 576 | 577 | Returns: 578 | int -- number of flushed data 579 | """ 580 | return self._execute_command('FLUSHC', collection) 581 | 582 | def flush_bucket(self, collection: str, bucket: str): 583 | """Flush all indexed data from a bucket in a collection 584 | 585 | Arguments: 586 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 587 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 588 | 589 | Returns: 590 | int -- number of flushed data 591 | """ 592 | return self._execute_command('FLUSHB', collection, bucket) 593 | 594 | def flush_object(self, collection: str, bucket: str, object: str): 595 | """Flush all indexed data from an object in a bucket in collection 596 | 597 | Arguments: 598 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 599 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 600 | object {str} -- object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 601 | 602 | Returns: 603 | int -- number of flushed data 604 | """ 605 | return self._execute_command('FLUSHO', collection, bucket, object) 606 | 607 | def flush(self, collection: str, bucket: str=None, object: str=None): 608 | """Flush indexed data in a collection, bucket, or in an object. 609 | 610 | Arguments: 611 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 612 | 613 | Keyword Arguments: 614 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 615 | object {str} -- object identifier that refers to an entity in an external database, where the searched object is stored (eg. you use Sonic to index CRM contacts by name; full CRM contact data is stored in a MySQL database; in this case the object identifier in Sonic will be the MySQL primary key for the CRM contact) 616 | 617 | Returns: 618 | int -- number of flushed data 619 | """ 620 | if not bucket and not object: 621 | return self.flush_collection(collection) 622 | elif bucket and not object: 623 | return self.flush_bucket(collection, bucket) 624 | elif object and bucket: 625 | return self.flush_object(collection, bucket, object) 626 | 627 | 628 | class SearchClient(SonicClient, CommonCommandsMixin): 629 | def __init__(self, host: str, port: int, password: str): 630 | """Create Sonic client that operates on the Search Channel 631 | 632 | Arguments: 633 | host {str} -- valid reachable host address 634 | port {int} -- port number 635 | password {str} -- password (defined in config.cfg file on the server side) 636 | 637 | """ 638 | super().__init__(host, port, password, SEARCH) 639 | 640 | def query(self, collection: str, bucket: str, terms: str, limit: int=None, offset: int=None, lang: str=None): 641 | """Query the database 642 | 643 | Arguments: 644 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 645 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 646 | terms {str} -- text for search terms 647 | 648 | Keyword Arguments: 649 | limit {int} -- a positive integer number; set within allowed maximum & minimum limits 650 | offset {int} -- a positive integer number; set within allowed maximum & minimum limits 651 | lang {str} -- an ISO 639-3 locale code eg. eng for English (if set, the locale must be a valid ISO 639-3 code; if not set, the locale will be guessed from text). 652 | 653 | Returns: 654 | list -- list of objects ids. 655 | """ 656 | limit = "LIMIT({})".format(limit) if limit else '' 657 | lang = "LANG({})".format(lang) if lang else '' 658 | offset = "OFFSET({})".format(offset) if offset else '' 659 | 660 | terms = quote_text(terms) 661 | return self._execute_command_async( 662 | 'QUERY', collection, bucket, terms, limit, offset, lang) 663 | 664 | def suggest(self, collection: str, bucket: str, word: str, limit: int=None): 665 | """auto-completes word. 666 | 667 | Arguments: 668 | collection {str} -- index collection (ie. what you search in, eg. messages, products, etc.) 669 | bucket {str} -- index bucket name (ie. user-specific search classifier in the collection if you have any eg. user-1, user-2, .., otherwise use a common bucket name eg. generic, default, common, ..) 670 | word {str} -- word to autocomplete 671 | 672 | 673 | Keyword Arguments: 674 | limit {int} -- a positive integer number; set within allowed maximum & minimum limits (default: {None}) 675 | 676 | Returns: 677 | list -- list of suggested words. 678 | """ 679 | limit = "LIMIT({})".format(limit) if limit else '' 680 | word = quote_text(word) 681 | return self._execute_command_async( 682 | 'SUGGEST', collection, bucket, word, limit) 683 | 684 | 685 | class ControlClient(SonicClient, CommonCommandsMixin): 686 | def __init__(self, host: str, port: int, password: str): 687 | """Create Sonic client that operates on the Control Channel 688 | 689 | Arguments: 690 | host {str} -- valid reachable host address 691 | port {int} -- port number 692 | password {str} -- password (defined in config.cfg file on the server side) 693 | 694 | """ 695 | super().__init__(host, port, password, CONTROL) 696 | 697 | def trigger(self, action: str=''): 698 | """Trigger an action 699 | 700 | Keyword Arguments: 701 | action {str} -- text for action 702 | """ 703 | self._execute_command('TRIGGER', action) 704 | 705 | 706 | def test_ingest(): 707 | with IngestClient("127.0.0.1", 1491, 'password') as ingestcl: 708 | print(ingestcl.ping()) 709 | print(ingestcl.protocol) 710 | print(ingestcl.bufsize) 711 | ingestcl.push("wiki", "articles", "article-1", 712 | "for the love of god hell") 713 | ingestcl.push("wiki", "articles", "article-2", 714 | "for the love of satan heaven") 715 | ingestcl.push("wiki", "articles", "article-3", 716 | "for the love of lorde hello") 717 | ingestcl.push("wiki", "articles", "article-4", 718 | "for the god of loaf helmet") 719 | 720 | 721 | def test_search(): 722 | with SearchClient("127.0.0.1", 1491, 'password') as querycl: 723 | print(querycl.ping()) 724 | print(querycl.query("wiki", "articles", "for")) 725 | print(querycl.query("wiki", "articles", "love")) 726 | 727 | 728 | def test_control(): 729 | with ControlClient("127.0.0.1", 1491, 'password') as controlcl: 730 | print(controlcl.ping()) 731 | controlcl.trigger("consolidate") 732 | 733 | 734 | if __name__ == "__main__": 735 | test_ingest() 736 | test_search() 737 | test_control() 738 | --------------------------------------------------------------------------------