pax_global_header00006660000000000000000000000064143434513170014517gustar00rootroot0000000000000052 comment=1227bca5800a2a2b398a6374f829e7675c69af67 python-nubia-0.2.3/000077500000000000000000000000001434345131700141365ustar00rootroot00000000000000python-nubia-0.2.3/.flake8000066400000000000000000000000361434345131700153100ustar00rootroot00000000000000[flake8] max-line-length = 88 python-nubia-0.2.3/.github/000077500000000000000000000000001434345131700154765ustar00rootroot00000000000000python-nubia-0.2.3/.github/workflows/000077500000000000000000000000001434345131700175335ustar00rootroot00000000000000python-nubia-0.2.3/.github/workflows/ci.yml000066400000000000000000000013461434345131700206550ustar00rootroot00000000000000name: Nubia Build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install poetry uses: abatilo/actions-poetry@v2.0.0 - name: Install dependencies run: | poetry install - name: Test with nosetests run: | poetry run nosetests --with-coverage --cover-package=nubia python-nubia-0.2.3/.gitignore000066400000000000000000000002111434345131700161200ustar00rootroot00000000000000sample/Pipfile.lock build dist **/__pycache__/ **/*.pyc *.egg-info .mypy_cache .python-version .venv* .eggs .coverage .ycm_extra_conf.py python-nubia-0.2.3/.pre-commit-config.yaml000066400000000000000000000003451434345131700204210ustar00rootroot00000000000000repos: - repo: https://github.com/python/black rev: stable hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 4.0.1 hooks: - id: flake8 default_language_version: python: python3.8 python-nubia-0.2.3/CHANGELOG.md000066400000000000000000000021611434345131700157470ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.3.0] - 2021-12-01 ### Added - `Nubia.run_async` added, the original `run` is a wrapper around it that creates an event loop. ### Changed - The following functions were made async and might need to be updated in your context you want to upgrade. Internally Nubia checks if they're updated to be async, but if you're using them in your own code, then you'll need to update them. - `Context` - `on_connected` - `on_interactive` - `on_cli` - `Listener` and subclasses `Command` and `StatusBar` - `on_connected` - `react` - `Command` - `run_interactive` - `run_cli` - `add_arguments` - `CommandsRegistry` - `register_command` - `dispatch_message` - Poetry was introduced with locked dependencies. This may introduce some conflicts in your dependencies. python-nubia-0.2.3/CODE_OF_CONDUCT.md000066400000000000000000000064341434345131700167440ustar00rootroot00000000000000# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq python-nubia-0.2.3/CONTRIBUTING.md000066400000000000000000000034111434345131700163660ustar00rootroot00000000000000# Contributing to python-nubia We want to make contributing to this project as easy and transparent as possible. ## Our Development Process External pull requests are first applied to facebook's internal branch, then synced with python-nubia github repository. ## Development Requirements The project uses [pre-commit](https://github.com/pre-commit/pre-commit) which is included in development dependencies `requirements-dev.txt`. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `main`. 2. Run `pre-commit install` after forking/cloning the repo. 3. If you've added code that should be tested, add tests. 4. If you've changed APIs, update the documentation. 5. Ensure the test suite passes. 6. Make sure your code lints. 7. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## Coding Style We use python [black formatting](https://github.com/ambv/black). Make sure your code is formatted with black before sending the pull request. ## License By contributing to python-nubia, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. python-nubia-0.2.3/GETTING_STARTED.md000066400000000000000000000112671434345131700167560ustar00rootroot00000000000000# Getting Started ## Basic concepts The building blocks of a simple Nubia-based application are three pieces: * [Nubia plugin](###Plugin) * Application [context](###Context) * [Commands](###Commands) and their [arguments](###Arguments) ### Plugin A Nubia plugin is an object that implements `nubia.PluginInterface`. It gives you the ability to configure the behaviour of different aspects of your program. Take a look into `example/nubia_plugin.py` to see an example of a very simple Nubia plugin. The table below gives a short overiew of `nubia.PluginInterface`' most important methods. | Method | Responsibility | | --- | --- | | `get_commands` | Provides the list of commands exposed via Nubia | | `get_opts_parser` | Provides the top-level argument parser that handles common arguments | | `create_context` | Provides a context object | | `get_status_bar` | Provides a status bar | | `get_prompt_tokens` | Provides prompt tokens for interactive prompt | ### Context A _context_ is an object that extends `nubia.Context` class. It’s a singleton object that holds state and configuration for your program and can be easily accessed from your code. ```python from nubia import context ctx = context.get_context() ``` The context should be the only place you store your shared state and configuration into. For more details about context and how to use it, please read context documentation. ### Commands Any Python function can be exposed as a Nubia command by applying `@command` decorator on top of it. ``` python from nubia import command @command def foo_bar() -> int: # becomes a `foo-bar` command return 42 ``` By default, Nubia automatically generates a command name from the corresponding function name. Nubia translates both `snake_case` or `CamelCase` names into `kebab-case` names (see the examples below). However, it's possible to override this behaviour by explicitly supplying the command name. ``` python from nubia import command @command("moo") def foo_bar() -> int: # becomes a `moo` command return 42 ``` #### Subcommands When building complex CLI interfaces (e.g. similar to `git`), one may need to group the commands according to their purpose. Nubia supports this by allowing Python classes to act as super commands. Applying the `@command` decorator to the class itself indicates that: * It denotes a super command * Its public instance methods are automatically treated as subcommands ``` python from nubia import command @command class Daemon: """ This is a set of commands that run daemons """ @command def start(self) -> None: # becomes a `daemon start` subcommand "Help message of start" # Starting the daemon ... @command def stop(self) -> None: # becomes a `daemon stop` subcommand "Help message of stop" # Stopping the daemon ... ``` Furthermore, the `__init__` arguments are options that will be available for both sub-commands, each sub-command can have its own additional options by defining these are arguments to their respective functions. ### Arguments Function (or method) arguments are converted into command options automatically. You can use the `@argument` decorator to add more metadata to the generated command option if you like. But before we get to that, let's talk about some rules first: - Function arguments that have default values are _optional_. If the command is executed without supplying a value, you will receive this default value as defined in the function signature. - Function arguments that do not have default values are required. - All arguments are _options_ by default, this means that you need to pass `--argument-name` when running the command (in CLI mode). If you would like to have the argument supplied as a positional value, you need to set `positional=True` in the `@argument` decorator as indicated in this example - `description` parameter of `@argument` decorator is mandatory. ```python import typing @command @argument("hostnames", description="Hostname for the server you want to start", positional=True) def start_server(hostnames: typing.List[str]): """ Starts a server or more """ pass ``` Since `hostnames` is defined as a `typing.List`, we expect the user to pass multiple values. A single value will automatically be lifted into a list of a single value `(x -> [x])`. Lists in CLI mode are space-separated values ``` my-program start-server server.com server2.com ``` In interactive, you can do any of the following: ``` my-program start-server server1.com my-program start-server [server1.com, server2.com] my-program start-server ["server1.com", "server2.com"] my-program start-server hostnames=["server1.com", "server2.com"] ``` python-nubia-0.2.3/LICENSE000066400000000000000000000030031434345131700151370ustar00rootroot00000000000000BSD License For python-nubia software Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Facebook nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-nubia-0.2.3/MANIFEST.in000066400000000000000000000000661434345131700156760ustar00rootroot00000000000000include requirements.txt include LICENSE include *.md python-nubia-0.2.3/README.md000066400000000000000000000115631434345131700154230ustar00rootroot00000000000000# python-nubia [![Support Ukraine](https://img.shields.io/badge/Support-Ukraine-FFD500?style=flat&labelColor=005BBB)](https://opensource.fb.com/support-ukraine) ![Nubia Build](https://github.com/facebookincubator/python-nubia/workflows/Nubia%20Build/badge.svg) [![Coverage](https://codecov.io/gh/facebookincubator/python-nubia/branch/main/graph/badge.svg)](https://codecov.io/github/facebookincubator/python-nubia) [![PyPI version](https://badge.fury.io/py/python-nubia.svg)](https://badge.fury.io/py/python-nubia) Nubia is a lightweight framework for building command-line applications with Python. It was originally designed for the “logdevice interactive shell (aka. `ldshell`)” at Facebook. Since then it was factored out to be a reusable component and several internal Facebook projects now rely on it as a quick and easy way to get an intuitive shell/cli application without too much boilerplate. Nubia is built on top of [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit) which is a fantastic toolkit for building interactive command-line applications. _Disclaimer: Nubia is beta for non-ldshell use-cases. Some of the design decisions might sound odd but they fit the ldshell usecase perfectly. We are continuously making changes to make it more consistent and generic outside of the ldshell use-case. Until a fully stable release is published, use it on your own risk._ See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. If you are curious on the origins of the name, checkout [Nubia on Wikipedia](https://en.wikipedia.org/wiki/Nubia) with its unique and colourful architecture. ## Key Features * Interactive mode that offers fish-style auto-completion * CLI mode that gets generated from your functions and classes. * Optional bash/zsh completions via an external utility ‘nubia-complete’ (experimental) * A customisable status-bar in interactive mode. * An optional IPython-based interactive shell * Arguments with underscores are automatically hyphenated * Python3 type annotations are used for input type validation ### Interactive mode The interactive mode in Nubia is what makes it unique. It is very easy to build a unique shell for your program with zero overhead. The interactive shell in its simplistic form offers automatic completions for commands, sub-commands, arguments, and values. It also offers a great deal of control for developers to take control over auto-completions, even for commands that do not fall under the typical format. An example is the “select” command in ldshell which is expressed as a SQL-query. We expect that most use cases of Nubia will not need such control and the AutoCommand will be enough without further customisation. If you start a nubia-based program without a command, it automatically starts an interactive shell. The interactive mode looks like this: ![Interactive Demo](docs/interactive.gif?raw=true "Interactive demo") ### Non-interactive mode The CLI mode works exactly like any traditional unix-based command line utility. ![Non-interactive Demo](docs/non_interactive.png?raw=true "Non-interactive demo") Have your `@command` decorated function return an `int` to send that value as the Unix return code for your non interactive CLI. ## Examples It starts with a function like this: ```py import socket import typing from termcolor import cprint from nubia import argument, command, context @command @argument("hosts", description="Hostnames to resolve", aliases=["i"]) @argument("bad_name", name="nice", description="testing") async def lookup(hosts: typing.List[str], bad_name: int) -> int: """ This will lookup the hostnames and print the corresponding IP addresses """ ctx = context.get_context() if not hosts: cprint("No hosts supplied via --hosts") return 1 print(f"hosts: {hosts}") cprint(f"Verbose? {ctx.verbose}") for host in hosts: cprint(f"{host} is {socket.gethostbyname(host)}") return 0 ``` ## Requirements Nubia-based applications require Python 3.7+ and works with both Mac OS X or Linux. While in theory it should work on Windows, it has never been tried. ## Installing Nubia If you are installing nubia for your next project, you should be able to easily use pip for that: ```bash pip install python-nubia ``` ## Building Nubia from source ```bash poetry build ``` ## Running example in virtualenv: _We recommend setting up a separate Python environment using a tool like virtualenv, pyenv-virtualenv, or `poetry shell`._ If you would like to run the example, install the dependencies and run the example module as a script. ```bash poetry install cd example python -m nubia_example ``` To run the unit tests: ```bash poetry run nosetests ``` ## Getting Started See the [getting started](GETTING_STARTED.md) guide to learn how to build a simple application with Nubia. ## License python-nubia is BSD licensed, as found in the LICENSE file. python-nubia-0.2.3/docs/000077500000000000000000000000001434345131700150665ustar00rootroot00000000000000python-nubia-0.2.3/docs/interactive.gif000066400000000000000000002622321434345131700201010ustar00rootroot00000000000000GIF89a+|+6*;tFskvaDDDgcW+6ClWH 8[Scd3Igee6B 6J6C 8H⫒rRlg+[_V_R]࡝ʯԮ~а]ڵڲU̯Ћu_o=\ ! NETSCAPE2.0!P,` dihlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~mne 4. + $)? 1 +(&ɯ0*  '}# ͹* NHA2 pXb߀x ` `&95ʚL 5 B"/P"3h $CNg]! ,rB` `I,#kJS.Dd;WDE0C6 ba#ന,Necd!(,yf` dihl; p tm߮,|@FϦ ͨ!`JUOr)Dm22AYbP"%Y J%c u{u @ fuc xx]sj ^KJ{'emIUTzrTj{jVw{sL}O1&ukkai"_~V$ (Y%qdn$J=YX)Jֲy $h de0_"1(Hp)Vܶlig"j[ *(.*MI)HOd J]"L^A$d7|8,+?% UW#! , ` dihlp,tmx TdmTEձjxGsš*]΢rjg)aox{SN'Ps/*jG = :d5:#9"/ )v(#̶TȾ$"Fӭxx .ҽ$/Uڥ2)Z;ta 7QP+QF3is 2kebm{ 0XI LR^T$ٞE5K:Sc(JDN Җ$l9Ic2QݸkRTO^I0fQKr%7@)#,&~mD *nb ٯ +͒EeՐPYQdYf`)қs]ʘ Wڋbio6|WLfYqFm n`r{n!>ٙmJ#Sؗ4 ?!/vqceCC#9 (F >@'LKxq9%x\d2 RaMCRq̙4)q3҆b`h |W$"qB WAF2 ZW=T p8JEY8-tO $WI~!d_*` ל A2@Brc# 5X4#7,l;w^qF9Y,aRmf](q 2dGZeF=;JpsR%W&fphaJ*mlIr[|í%^uĤX,u#clѪf骖C\=_*Ϊ ql,e`C ?2{l Nh eb|i5϶v!l8"Pe֠'XwءV[yfšVZt$ʺ<] FDzX3e6 l>?x-'~F:?y-{{ͺv: yӺOc+@>I`+.`f4;Jtm$o]m`F濊/h.2&zcދ C 6L"@mlXK5{ Lzq  \0Q)L&L*tja]@ P?p}jN5Y^tC,^b5j]aY  5 dܓ$Ӻ_' 3@KG\0D+< 0-5ŇԄAb)CrQ%&ꈈx2H+Ibr RNN+d gO*X5#Sљ1K 5ەn9 1:&U͜Џq @$h6O\ 98ah=;WPBKi("F}s_z]:iN3K4I!:$%^aUuM07@¤b jb0y%]zz f(FMrIīJei C|T%pSzcMUyQӜ z-*ذP-VEk`”>-}?k 4Ξ9hO"uP' xB\ZH EckY O&irp;5'Z<B,b.qj\@n9z xK򢲥MzDKpˁNM[,'Rΰk { ~e+(ASgLkx6αw_@rz},"Q 2&ANSXβ B!,& ppH,Ȥri8tJZجvzxL.zncO|N~ KkpNK jQSK   Bȶ ==K  G Hi(q SCfDb^BN~֚BHɓ(#2(Y)ݐfjF8pR JD'&әL"ķS@ʵׯZ91A q4TˈZX˷߿Bk bN+qa1˘3r6.KV*V"UIeͨS^>*Y˞M+xhaͻߤbNȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z +Ƞ7z GH0'H W0 gH8̡w@ H"H $:PH*ZX̢ !p$@@2, ST! %jpH:@ D# |  Y  gH(@2A aNʠ''YPL*WV҃z"CHIR4a`2:O  ,1[@LWZ̦6M2<@ : I r̠قDQ$ yc lp,`-8d/NSdA>iP$/YvͨF7Q * ؂+0өG-(#A9_4|08.U?*O@4$1m %=i ELbJ5BUV` XJVlY Z ՜+cZtR4V0dPOA']ip+MOHM]U& ɲz h?5@S*o5MQ:Ic mA I|6hdPN(uΰV E Qͮv KV95NæzLmAGְM,! Ǘ6lueeRM!wL]!]t}22X ,@1hLejⲟecz =Μ `V,e_! PNWlL"%*Hvҝ}}dJ[ǰ;:;ӓ=5$'gpɞq zHM?WL:F ʷd_LȘFek2A ^ bA.rȍ  5 & aOnt%2ćB8¹gtw9]fb<_8os?`SLEOuBUkvUW4iu3mTv1Q{mTpA-zFN!$v%6sA8i& HHip=I@Jך=@(HNh|֔@^iuiX|ۙvܢghaޥx.PuF`i \8Zs 'eRꛯh_]&iplI:[]n6&Z$LIg8)L[ӜV@L%-RgF{Z[*X!#%`varpZcױq[Hu|JHl&JЦzܩu mG֠B%%u9ms>CX%Fs> AV\t K%ip6>\4=X`)vo7wns/&݊iS3p&p^t}וY'}/Z dϫ[,vn6nvj_]7zDYM󇒐"l*RUٚrpMƁSW%!+S5aS ^kk`5/z47<ۀ2n0y^tB^\C6÷@nYI@87au  ʙ<)i62u8:aCHavH@m ] "IyR"8I=?(rDp}Pⷲ?V .'2l+7O"7KSf{4@Մ=ƁFTL:2#Ke2/!Gf#@>`&fҮkU 7l-eEGۍ8Vkk['|IUGӻ/nneڼMsP`ZJ8|~JF@eŠ;Z3Fl^В^EaomDa2dWG1,wk#^ -oG;\v!8zg[tnѸrg,5K.&4eYXsS?EJ87{ G~(OH0'3s<8Ϲw<(_ ۜz 崣 F1yxb*[;9_bm\3^ hn$Ij Dq %P,'X '݂s' S]0c5 rMD7wAs[i4kjpnj''J'-N}P¾@M-*~'U}1NFBR1D`J}iη$Ɋo4u4=_ ;r!jR *,\Xס”?TLq^0}* )^ޔ4q%,2OfV-Qw8%#e:-rdR~>SF\2gT0RkDڶPf-1c+8&j:#fC|y#&)s25Q1OLh}Z#9)hT4ٕfkuz658Wfd7z|$sF E!75vS6au^uCufV#6h|)O!7d9fvX_ŔoNRve9rEdLvb[:A+AHNhH=%WvuCTy8B"~^8~IWib96T#D:H4WEN$K)4DUh]tSf[5Ma3a6M(XB@P{oS4NEsa]4=xDe8g,F!Q\}\H㇅uP+GEH!H.nOQ+!eC0JUSHoeAQhHkyD<f2gL PnW%MJz1V{PVVNUddNieGU OOŘw PTORO,TGJi{Iݲ; PXgDh|#!=Y02ѧDi6oe05P4E y! S<5">aŕ镁U׏/MAeV 8]ԴuOF_Fw%|qd."VafCd.vVy;"4QuuXv "אY/Q2BbYY?QHX"[5^{`օcxGe{jڅ6eNXÝ_H]uV%4A6 ,׹SHj*8gQ_ ,8_uM{\<&9p-ZjlB"_bc< f-Z09 G xcjag!!lfA%+FbP&Qsc2KRcf/:V1z[+•BJEO'?ieojJJ~ڙKkgdŴ7QgqyHZhOfT&yz!89i2fj8e jedjv&,jeI&kg6t&$a Vvu[[چ lmlfnbRvz='p:gѰ&w"V)˱j6w֜8 p O>9i6;HJ-#A!&,U&0@p(rl:ШtJZجvzxL.:M$" Ƶ|N~mCoql$I~B "E$#"[% pGԛ#BHgɯ %#$# ۞HCC JwÇ|ŋLdǏ6IpR\ɲ˗0cʜI3&8sɳ@ JѣH*]ʴӧPJJꂫXY=ɵׯ`SVKٳhӪ]˶۱YBymaoآzJu0N  DgF hi%;Mjgj+equ ^/TWe z^w ) pO[O ͅlMQVQ Ya{d #C@G(ď83w˧3>҉gO~N[0FWfyu[4&Ak V}:q6r9qC-ATQ|AwVZw,IНa!yFݨ K dkܕr,[pktduQbAJ4&p7:=ro>]LDBFފ+JaG!tەȸ?n&wyuZ@_k[F_l髳r̩#*pωXi5wJADsu&z1zS{ ,APrd>Wi;㫿RݳV3Z5;dpoQ+7Y(ݸ@dV/%Ɂrrprـ pk puҗ d|q4 @|͹rigw8tVߥ2W#v+yA}RSq:d]뫼`]9z,q+ը]@p7}mO8(]p׬Xv:-v<2qp^pi$r?} \b@Sm@|nQNZu kj^`|4k;to(ut\j@b7NrJǥ@ȳ&q1Z~E3_qd"<|AR0zt |G 4<Np:{$$0i8^N[ʐi@aLHH:WG:*T\} .Ђ9]ql}YJUzC+ T^¤p炃dg/ R b֩.ꡢ \&`[>El|UMw& x`d@@f$*zwO6w5E%bE8Rc))`p5᱌I׀^rSfGiuC2ש*x^na1϶ZB ^[e+-E%RwɟTNd:1:z2fF M8v14` @0J 5;VZnD(,lYk/j[<#wY3rRȍ9Rf^2ĿlRzq3?C ģԹ):C8ɵX˦BI-R0~PEW3#nNP%6w T󛣴ON E-F8O<(t!b.R>EFqd*Sןhh؝kg D!Zh-*4p)j0Ӄ(@U&?rB7ʜq'N-{`YGÄC2'5Gr&tSLX0DF,@%UkI)+v,I,M+TqUFk0 I^ɥէ|BLqVlEX/P Sl~nh0e4<1e2kb%~^m'pzu>-*kɑN`gRul!* ՠlHj}40ScqE-M.H+VIaJe45ciG.F dYm,!tR](U"u7O-z'e =-'/{YZ);7n{ G^$(O9W0q\b<8Ϲws.vmn0Oi c75&(Ӆ2uG)V{4Yc䮴7 µj=ׂsL]gGYUwT=ʾi<0⅂V )nL?V&gG% qR)Nr;5xgag,׹SqldQPs"eH)R2I dD6MG(¨4#)Pt&GYjQ8Ŭ wu+Q@޸PySLMTߎgkG,0,6'ZQ'}@d,.[CFR+F*xdeHD:xG'UA4E80&.[8"}gGYwc9cE!S2Hvyηj@C$C's4d8ED1t; BZ#VK618TTj:3/9o7%]bt={S9{`4MQ8sQRf>Xl!#%@քΦNS yU}Z[DaF{KNez%='`ĂO \ۧ}ݨC@dNktC,UfcՀfu^[M5NX$F(Wguu4Ey(eδE~X }VNFPA3)u$Gj;e5z4xg|EH>2>&5>E.@cvJJA%zh]UmC+tXS޴)`"rW4iBLlbPU5PivhMtEבU{Ovv{7ˁARCāN%U6UpW #P_`Pn[%䅾tØ4(3R!R@ʧ9>)'!.SGYXX|eFՔf]tWj9yy)Dŕ´DCT0|#VDckXX {H"y-t5dLtX[)kxKO9F-cm?n0Q"?ZY#%2`ZL@yW (&mV5_呒(`ѥjgu]t=Ug"b(^řȍ2]FMY44n:C݉xl]7DpWiW%+%y ؛5aD6 a$T5(J:vWu`ubE=W#bQ)p<%Sc&`mk gP*Uf7HYfsVJ,tCI]#*ggיFJvo6iqBQ8i`)vDKcAkB: YŝhWϳ $pfEii|'yRxh6ʪ"iWāƪl6>z[VoY'z`pfV**Tn'oS>yʚw  fy! ˮױ2 q{{#۲.0+v!<#,N& pH,ȤrL PAsJZجvzxL.z|N|!9 ^"gmOzE <<;W_И'b@Z24mIWT"a)u Ί/F #HL2[Nd$0t  6hZ'yJ{]O'LKI!7Vq _شG2sؘu9MLe>]J[R"=6㙳 ]ﴣ`9pf%!M% p~dلQ =D@5$l{AF*`^ j\ @4J'] RdvщPTb\.3IvӲ/+%Q$j|+y)/\4rf2ef*֩IWjPl'nuIQfM `Y d-m~^f:y(e,j%,]Ӥ^d^̰ )i~i 4u ٬ʈp4^8d/eoyn55\=2JrL dL^[jފ%XmP ) OxѠbp@2KpaݸtVu]2* v K梱Yj*a=FM*:9>Ԭ.?o3M7 `SeTYǍgJ2Xcsnȏtb+lNEKwKhqzE!-^ۡ۸&P1R,Ov:D5L7*`* 04 tC4Wc 5mX˳ XS}4Uof:?nYVxfPW; l:I4~_%˾d+9nm#W@y^R0*:ZdZڒ;CLYKhnu_-]Lw@_$J}Ә^$`)F_MټtX2g0 p GR辤q d ory}]9?*Pu_}[w n1 Z];k޳˽)>b疒C9NC%c[5CP. @wU;,S3w7#j nXܗڴUAVGM"+o(&^yn}2w>c~y7@ir}}5("1:'!8TxBb$e$ _b.4 `DX9N @`4жw.`:)⧌ 78@d[9)nk n Ҳ.&x߿_q a)u[Dfao|~$ƒִu!MF!84aX"&~ gi~}ߊ ]v bwn49_CB@eMm@j)6& {d{Y c||h)ZJ)2>UsRBam k}gP_1Wifg!fws]h<svfj}(*Ii\th1$Nj9צYYrza &)hcyqښ)yӟ:ubNi@eI$fa }w^i1hv:ㅹV]ݡ>/X" Jʜ |hor.i\JXoP:'vD nJ2%d-Wbv.fo-|Zi!oݵ$:%/kL25og@D߹$?*i}#J3qMHCзt6.p6t#n]0a @ER cz{ 6NJc>.Qk:UòkKPWM:1_1 W׵ z QRɓ'9I,wY 0˟-XyewI L&Pvf Д2iZd3hz% 8IN̍7ɜr6IO #Y~F- 'L03:/EыӝDIҖ ~] f`` v$!}8#:і~n ~1TCeNj_CMjq̖⣒^atGH8ښnBSLҎePPUbU!UdA-n{XtFE`'&^UZf  :XrYԴ$ 2[&Q kȵ9ےd5ѾT+&h:[ֺ HmPJ<]iK\¶hD*uQ]0x9<3Mk0'$o]BR1seͯC*hH.(kݍ D}cM7hs/E:Sf麵T:q>M31U_P #Usz6FW@wF8IymV`H +f1FpEZPaXXA>5|S_  !!,&b0prl:ШtJZجvzBL.zBخ~_}qas|D~DsIQ~tw S|` Ηc؎  H*\ȰÇH 3փȱǏ CD@tȲH(@Ú@ɳgG @84) 4@y/c:ٯA) `N8V!P/+[ *D@;xH^0ρާ.ةk~ 7[+eƅbg"ъK!}3rq`8`{p0T;& #6$WF2J)?hs^~d9h ( 6[+ų_4R׊BbPg$e#Puƣ2weSK55B0֚-F%dA>ޚy`j|v\yu}:%?lÚٱA~hbe$Mxi@z8G']c@vч*Yv"(p &0?PZHH1=,1Ͻ8›A\<"j$*)H"8FnC\R(ILHD!66LElFn H2"[[aƏtpcx5NqiṛG}б IBL"F:򑐌$'IJZ̤&7Nz (GIRL*W/!!,&b pFrl:ШtJZجvzEL.z>|NwNzRF aor  S Bv_ RF b HͿ  H*\ȰÇ#J(pŋȱc> CTFrɓ(Sxc4HA %Umt :RQYZtyB 2%L ` h`hD NX@vaа-BI2甪= sj?o TuPܓ]9^ *\~,c)*ͨSЃ4A(@myzxv5 ]G 3Cк6weԾ=T gl᱓=m*swkMkAW^[ǝ`ery[eVEzi6dǁS|l@4hjĦGtIOD#lHp4@ٙ?FM>n`pbۍ9<0iE $%$ޖ=BV2>V?>I#DlWlFIeR}z*HfdW=}mT#v0ճ@a8^8OبqVYPh%ĩfךQdxiMebIes"ؖnP@ٖP{lysmFXڄPU kѳi= Z䋯q[$Gpъc]YItͪK؀1z%Ie/X X[jmkuXd^k6qB[P0Nk.M*)[8[اz@ۊj@Ag/ť_[UG]M ]6¢5b P4H4B`-BG+]CQtFkA]`+\0 V/c-YVUyfڌ)A|Z➕͑Z$ۚ\ȵAP}ݖO610 qңD>ǸG@?ϕMk1/qzΡ')@gqS G ^I|@p<8Ae\,v)a/P nz bOB8k ]\-' A(WdH(Ńl CFT1Z#I6.Q@,Ƣlxc89*z IHb$!,&b p(rl:ШtJZجvzEL.zF|NwOzRG rRDo~bf HS Cؼݣ~  H*̷GLIH‹3jȱG6Cœ'=b/%@(50s(!tw\"uzk_;Nht𖽨֣U Y]MoAe]ѣ0P\w:8J">l;R:[Vl]8whXGlhz(<0uVؔOkI۷h&h4KA`P-3ruf5-{즀5sGݺM!UA)M~Q}tQIepWqA 0NTl֚Em vYv$& 0&5 &Q5S)X=mݎ=n `"̍etSA@O$S)(PnUO%_ zbjAORr&Cs\V[I'Qwq:aPlΐL > VjUQw1*QXzw&?!` `F dإW\cOOjϵU۲jPS]W>З{>b[O&e prܑjd5w'J #,ڳ0 }VޓK:|rnO0K${50>Ymi412֗@Zxl:{g˩jwe0r VRXc-e6>B _Im-EEQhz9S-2&ӝ lSEz} dog51p\qJȭƭƶWS  [$;W躠6>c; r)OhJemay՘O< 6Sʃ$|܋Ki]5ĩYtuVLEUb&S31U7"H@R3B(~}K[v, }Xiqb\!ID:0{1GHSb؏סWq"&3ŀ]!â0B-,p4:*1 >qv $IHBK!!#,&i pH,ȣ`\&ШtJZجvz`h.z>A[8f~X1rX4VuN}]X0T}-22/ ,,.30FY,"/266 2* - * "$澐'˰- 6B C" ԓRwNP XAŊ ȱc? A2^=0L3p4zI&2aІr LBB2idڜJu&@d,MrNԪh΄P@gڻxZ:_ l30ÈEȸǐ#Kl˘3k̹ϠCMӨS^y װcCfM۸sޝ 稼sWh:d{ ;ODk87sOа}t tmA}e^-Pc!c ™zv!-w p vM$bb1D#j6Q*p wG‡DdA{h` \ vp͂\P0Aup`G %yIIP"6m\1sAǠ}rv )@ϛ(C;sl)E,)T]5II%bh@ӊ o0) ߍ:^}+A<=]%-oZp항ssqB'%^u1& 1Eek`U,TCMF;El( 3,fiۜ|VvM5s(F継۫w{K9(@窘e>SE.f+`a MxǤ:zДFrj43,!,&A` `&9*[‚La2s AA\6\ Fl ,k04%ѣhgK !,&B` `IJ8m#XJ. fXl#*]z@. C!, &4pyT Fg*&*Uf#I`0Yl05쯷KKh!,'&>` `9*[2`.v'GH 3VA"W:NiℍDƋʪMX!,.&?` `9*[2>+`a MxǤ:zДFrj43,!,5&:` `&9*[‚L (9pD@*Dh Ѽ] ! ,&lpH,Ȥrl*ШIZX4:a0/ѐqd36}f> #>r.?r4mwWmK9"uwy{BWG uy~`1q9q x qH"Z1 wyuBl1 |~ 4 ~施깃I1 8x~ 0('" 2SԴȴ,S,[Ϥ>|@@;?jVI*.df9IaE v'Q9ܴA$=iPECU#s KOL47224PfYQ@'BUnOo qȎEjvKRet KN,3 N9!nI8JY',x5m! Z2@,5׾DiOμ{Gg(!,D&;pyT Fg*&*UfOI` g@% ԃ}^PA!,K&!-6q^ǁڲB!,R&Cp*H$qHE+L[-[\jw:jrӡs2~v}]uyzA! ,Y&I` `I:[rp:6c x%Es 0Z u`KLzHmL_B!,`&F` `I:[ra23+`aA\%TBaqe%=/up7,!,g&D` `I:[rb&#ȰY{*,b1$oiåٰ!,o&d` $)Yc$7 ,-ǮgN-|X6yOQ].D}PJeP@qrG]+PB| 8>@@->!!,}&F` `I:[rb23+W QH`hބ x@NV F`}eo-!,&4pyT Fg*&*Uf#I`0Yl05쯷KKh!,&a` $)Z#견¥|ҵ-‰ D4@1BH H`L$oH|#k 9TV}NE % E:8-2"F!! ,&7` `&9*[‚L &w.8$r=E(-dр9Uպn]V!,R&^pH,ȤrL P@sJZQW!qϐCXG.6JC.G_A9 9"oQrtv~Jr}T fpsuw{{8{1n{fz9CIof}zd9ztv{ 4зB zG 8"1၀̌c cnFf} ^9 Bؙ%edހ:O~ yWDW8 oM#d;>i! HĢz8S_Δ␞]6ةL8G|D/Gh 4-:H]!Cj{Pf`~] Ol'y"]|I tѺI2 `2:.fnBse {Fރ,Yͨz 8ξ~]zݗ!!,& @p( Ȥrl:ШtJZPPxxL.zn|N~v __teC3W7BC ExwƳB8nyB ®JC]Քh]ǰᦀ!HBu3j.CVmƓ( ȑ0c /͛NB=8JDj2x ӧp;JdfPjMWWm;V׳ ÊB˶۷)9ȝK]+w߿ LÈ+^̸#/K!˘3u̹ϠC!925oVͺ e۴A$mĹ?A@u쿻f w+ް{{a!]p19aVݺ}ـx Zz4@s ECA W Ʀs@u 4ڂ %J "|g1*X][*~ƀ'T܊ ,Yx1Rn$ XuCz ) xDnykP8$H_4|&&x vvMn7(7FHBzFt~֝Fi v[d?&`eepeR`Rʅn|؜=G:H$y@Ȩ*;yG#t.qio/\JrE v`#+k.FAX$ZvXH_>akY< `O- )ym2|2}~ , 5:rA9v=f{1]vN?{Py|,y~*P_X&KC۝ O:.-;#t㚨\yrg$ ,_|D&)sC2HgvHcR{}{>nў]E^+0u\XVoN2K@/ HAPHy7z.BHBŀDY WX% g!!,& p( Ȥrl:ШtJZPV+z۰xL.znvNo0,- rzV,,1c}]. tb`/C2guŲaޢEKB}o 2[nUJ&Co`wE\$cDG!ֺ/,s "F3 QF"v*]fS|82Jӫ(৪ׯD-سhӪ nʝKݻrA˷߿ LÈ+k# sk̹Ϡ;K#漧Ss$׽^ 8f 6lƥ T}ĵ&2ɣM*b6kbEAךKS6daUنEI .53??m@PBJguBF6%_m6_/TlsڼMtKxUKA!!,& prl:ШtJZجVs]nbi3"Yhɤ/iJՀTr`aZMdmbqeZOA&eXQ1y؉(HO$x믊6)&B8rЁ`sWqxiHMz3*jWj$/YpI<*T[ض^7G6ۨ&# mjK Jash-%f]$5'wgE_ ʒ8K{NU `fK9T7jlDOykS<;[MCg ۴\ PaChvbDhpv-xg|IA!!,& p(Grl:ШtJZجvz,$xL..n<6MFtuo  VuE] J}_s jmG~a XO M Hpܾ*\Ȱ?a71]|`d Cd \!@&9d!fAٝ$o};VP$ў8sA͹(!#F噠F PN^⍔ 䙘`/c 8ĈЂ,z48'ietcuZ.0>y7pA,0N~`M.!,I|=!0 I6@ IC:_.^n} dGxQ$v%ǘ`[T5]!8ATǏgY!'Y>tD#7f[S88{_j5hS]W7END9xT^C?hOrN8g/`ek9c7JhY]X`=`iX񘢛-6o.%N[~L IHך-J_XgܘfDM㫯ӤsT$N*QS)Z)pmq)8yЁ̩L;-٪. K< %wٌ򉢻: ,.Y[[ob֠U.Kab:k(ՔFvq j7bk㉖i6jܣb^TƚJ9nE2Y2͔ltN|s`_Y8|"B242Y_IW8D,-6:\ӯSAUI=ոv}'xtބn=O!#,& pH,Hr)@:ШtJZجv-2ݰxL.I h|}tk6}j[lnt52,-"B3-54Cj0,,6Iz\3,/B1Bv,0"v- ,1"$H w,C2/ "ݣ37,C-26  {B̯ЄHC'/_)\XF\ Ȱa8lɓXԩ* !h)!%Q)&bJO΢H9S*LJ}jիXj݊5`ÊKٳhӪ]˶۷pϖJݯq߾ν{ˆU/XK. Y &D@0yʂ$6wZ!638w: 6vd t;lclޖ]n-0ݸh8\SY t6?ktD`git XFwd@X< ,A!2f!xdh2~%ࠈX4p>:ZdU#6` ayZpA  bdKYT7X}h@u0A+*&Xgfa-pW#YI^sjl &i)fejub y *6W!fC蒟UF6|䬆i_kaBXkE!\:`ؘu5k@+rj֯L-%X.AyYM$ ̝2KW&ǚcny^ʫ`V}Z wy(, chH :hݷK UZ@UrU!`VW4ZK\m \oqJpN9 43e!k,y#q** ;| |sWk 0[KҸePfM:w2if~l|V؀d Y@w XbY'WXf.-Rs}0;/*hh&ia䭁2;{|f.%Co" _sXKcyn]du3}o\kyqZXRZ:dyd"N&CYh:7 dn]9H00A!,& pH$ cql:ШtJZج֙LnxL.gBWin8Jk|)wyM (&xyb (O hvk  Cohu&fvbUFj H*\ȰÇ#JHŋ3j(!,&Wp( H$l>`LLlZRHtS!*0wiG9xg_& ]_)!tgkcYmiLovKBgA!,&Tp(,H`@:ǂLRдzkeFSc`y8ɀg29 O+W j{!'ouX&[eBSKjJCFUA!,& ppH,Ȥrl:ШtJZجvz8-zn|Nۛc}ryaYdPGϞyqD{ H* b8XG2fci8q"ɓ,DңDaXٲ͛8sɳ@ JѣcD8tH37r `昪LuVmʲ*̢J|H+ yF۷pʝKݻD2ŸST R*&0O6 &t1KEE<+˘3k̹g(u(L*>oÿ%i,FlǥB=z)14(6C1mvVW=w+;?[*ӫ_Ͼ{}-3j:LHxkXهqHw"!DGSaa aU%1_WLXB@E|PeXC ׍ݍͶ>arHᙸeb8rq`uaa[e #yV]"JW&gLB*hDA%qJ6[KPn$tAԨoɉXXiV6uY dy _3IV9t)i:譸QmޖU1=N&,~a6++>%I.ߦ圬.ڜyV嵢fD{++^bpƦH4zb`i(RwSar֟bVzQ/&G՗0dM&I۱N%ګ<3ziXkKߓ3؝UڹKELWcK_'`Z'VWl_^GMecD#|Q-XN$5-U c_8J5G $~IaTN]~,꬧c|RmPkSǾ-W'7G/Wo"TxEw==!f:xフFI7yU_FT#m+f5E'H Z&J)$EMJS+MiW <(# NV wlZ/ HDmF:e  XZ.}X4pd&xȀ(/ehL׸5y$&~eg?aȸϝ1Y 9)F~kChHH1)$ i4 &Nz (GIRL*WV򕞌 Pɓ@ 'jl&$.9"d2L<pL&QpuN2Lx"E9QreVƴL4l9rry((郋/w DT9TrSV5^vuqoԫjg&˚_m;-frKalݭ$ԍjU:~qf<%6C4٬1 3y#.8W[LϽ`V9I8 Zl3lqcb\|G[+Vjeз!m8IG;ݜ4[bXQL5oGysͧIa:zFUvQc%6v7WhGfPw^RwvPe\veF]G\d)S$d heȂeP>vz6KTTEv_yn{)cGN)FKNW&hPfa7K8sz^8UpKOvfk6Ez[|jA]8[[RT]aMkodfr|2{8Sh~83j& NsaPo.AvZp8"HxlT{,!p؉R$U ^TxRM֊X3x؋8xŘ،pm8ɸxب֘؍(㶍Xqx긎`ĎXF(m9Yy ِ9Yyّ "9$Y&y(*,ْ.0294Y6y8:<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ9Yyٚ9Yyٛ9Yyșʹٜ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFzHJLڤNPR:TZVzXZ\ڥ^`b:dZfzhjlڦnpr:tZvzxz|ڧ~:Zzڨ:ZzکJ:Zzڪ:Zz'ګ:ZzȚʺڬ:>!.#,' opH,Ȥrl:ШtJZجvzxL.zn|> ~tvBн#"W` e "Af_wCH$VYЏהFРᐟ Z`0eI$\8espVsM8)"ƂH&(DbzeϫXXz*ׯ`QyUUy&`iطJ%w,hБ\Lx[1cPL_&M²3GπԚahКSf9ך9u Bjظsͻ N\Nȓ+O^t#Mv:D,/HAeïŹ7a)s2|ߘ;ՙ1XFJ4݀6FxI^Ihfv ($h)@q۵TQg9Ax⎆0ELP DP$d$TrTDȕN E@Є^OY}8٤NAj2 %5)@8@BԹD.qDě#(bVj饘f馜v駠*ꨤjꩨ0aW8ZPIz$,)~k{&aC @\洃zj2+S޹D@ [ xmb,6bh~䷸>KDR)G f ,-ERL}f_\ &dKIl&$/j ׬k g;YG$d r :+l;,2Wݵbܫ xM'Kkg/c.``↍I͙DwIngw砇.˗'$饷o<=DT*BXHx?:T@wc}DlOzT5jsn'Iwfna;I /[?:?W&:$;;c_.,eGjeiJ4ⷷ)jvF?ˀt]`*hAfVl (Lc`n;\! )80s 5 DSư1k*bMW~_r_"@A`؂^(6? paHJt@=ů| F[al`$'VAr *ǎ ϦId:ܜTE󚊘fnI7mz 8IrL:IE&Mv̧>~Dt BІ:D?eP=N F7юz (@w2=)JҀ8` (RbZҘ"TiE{ PJԢHK%( $. H%UY#h@L UFQ鶪SN`9@MZֶRU)xjr& pQ ,:e\BS6"l;UT5)1zPÚ5jWֺf=Sc)-m^1"B"gUPIlNX=;Bն}@/:2mSEi}xK7T*kKIBW)]v҃:U ywCE2`L8Pij;^nGΰ#[h b(NW0gL8αw@L"HN&;PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{GOқOWֻgO=O?OO;ЏO[Ͼ?{?OOCH@) d`?@{~8Xx 8}"/ { z {1{3w(>{00(5p7,P:<؃>@׀聴wGg04`((~' -p)!p-)]X) a(|Bjl؆nWXP08%؄+8X6/30},u׀&-'؊W5v xP|؀6`0wUh{Rȋ/ +,* Xx6 `|5؅WX7X548Xxk؀.2Џyz|ȄM(+0,z-x!hŸyw.W]؂294YLJ؀-P;)YrhhhYX-Sx-2SXMɓ>i|(d{58gjlٖn9YLJɇ׀<{!x9y Hz|.2^x5y.-)OzG  0Ř&z5ى Z  X+,w83h2iYyFX$"ȋI|01؞(zQY|度-x )Yz}X! }uEKSw]w(":$Z&{ ',ڢ.j)/:4Z6 1}7<ڣ>!,NvpH,Ȥrl:ШtJZجvm `AwL.znC$rH,D~m0,- Bx oVTf Ocl B7,,1TfCp_ <<;RZcC<-Ԑ ,{ݲ|)]˄0u.J%t!ZX4aBq 5@O&Lo K_\u(Eع f>,G8$Hp#-֠I 4t@gIP)0 ,L42" ʱrbВi`94)OŠyac4ő!!ȇ99Ɔ ;!3AA3 ew9@я)Qf!bz08ü1-PLg00)k#.+>"-9!Br!2Q =yh`C|5ՁIg,2PC9J&DQR#:ьjf*r2r y Ә;Q`rb/lOa D'j%ԭcBXӖ)؍uc*Pje A"0[9ko V>TDT !JƸnN=a"(B0Zp+4{䵦B;@Қ '{;X8 _9Bk@PJnw{ ᠕HyK\HжMrZ A!,v#,!ƤUݙ7z!Jvx{q!,v (:1H &Zu)$&! ,v /xÖ3Q.iA戞X! ,v9pF.EfRX9DQ{2#TkU\<O@)TM[N~ϕwb~YA!,v;` `I:x0+\&5 K )|4d i8W|tB!,vI` `I:[rP,4a48C B-ɀnъx6 zeP-D%C!!,v^` $)Yc«Ҧ}و#`B",NT4v6`K쮆RxC;w}kK8D6Y2"E!!,v]` dYhZdԘ.>C׷/\! @ ! Xo tO+*z&((ٛc5f0v/@*-J5!!, vB` `I:[rb23+aA Pd- &\ Vߢ!a5j4^4WvB! ,v[` $)숾B;@ #0<-Ax:a7 C"\a%niԊf1x ӊE3`=ɒThJCuDؽjoL̳"s^>sM[1 K :f.ܾGii z[]3gA(ܱvc=޹{RӺ׭+4h9qQN3h1F{uG'^w^UJsn?m^}X_vʕ]Vq@]W[ZE7Fyi7]&"0wU\W5ptNi4 [/wW ڵbbXGVbi%`wׅfwZHFV7hIא Jڎm6pDU#4"6囩uԗ4bm;:]t ]*]@Z] 6Qiveؔid|ZgځZv!j(?i 橧nrj9(mFi\ƪںF>džt]mg,׭;qłΦn =Kp_zCm^es+,آK4GYc\Zv }WyjUyC|UU !2B-z~bdԵN9˻ i:RxxtP(qqx7vHqgNnF; |9qy4YWAW U;@GzJ=kY&1EAT@$>aH}JJ˺bAA T;L` bcp$ʅh"\AIKΟrH8.4XGqEup=2h(CP>mQ39H1vo]Fʹ!(0G:J)V 1؍aPF+as@`GZ̤&7I)@'GIJL*WV򕨬IBR .wVč)JDL2f:t@ C̦6OIWќyn'Kz̧*}s H@"cЀ&C}h@.4=pM HM5p8I-h> N|5 Nϝ,BP͂*xhR&EPFehX2Q # XJV7|38ŧ YSԞt?YJz959mR Mʨ`ը6oCT)F0KZ,H n}>YS`M[SCSЪBV5bc+^e({M\jO'sSY/k 9 Kr[a $Q4U|MrzEeJSږRWup6KuQUuPO1b{Qr GM؞n[Sun^w kx-rTN n2^XTfx/&+e]ת▲حi_]J^Oh q6y$dOVoX>΀veHr<ψNFRE'G[A!,N/ @pHb@$G##l:ШtJZجvzక8D*znx,L!FCX,ry~NTG }O~WzIlntEH P^SY]Zf PNGQ GXvxfwͨV\ΈxNWLMƎaX~*Pd!IfF6Y,HrdT5l(2 @CaF{4(ƘF6agJ\"M iƙ*&IN]w Q"D j9 [OS)=H*Iv)))m4(]B{&Jwר%6q6D{WQCG&UI48XD#FצC^?fMݳ;:z&z NVCMlR[v#nwMnDIzᔄ v\5 \FgtOIFv ]V7zߤ)n5CRq:DXm"( 흶y idn% ]cJ6WH~NN0.6yuz:S+QZr.yzzg\CSfWQ08PG\T4[3iStBVHbV#1a @·Q/xa#5XhoX꜑PSWSqܕu&.2Z$ԨJSzx9 pK"s6.w>*X.*MM~{h5 'U⣊N"KU_f$J8l"0B-f) oA k(RTV8Jk\L,qVQ&#qGDZ!MlsAެ3$2kFDmY\3L75cTWmXg t`-dmhl$Pñtux|mc͆U07^m'fS.}c^^(A$ h d' `vI3?{ 74>d{z5'?6se96~O~W_Pp?\ğ:@83U<%Qw, .]_V]D{ۓ:e,k >^xk3XHhB ~ |Ԁ5MuˀL-6Mz0@$7Hr d@2vݑ &zS4Lm2t-B`&،74԰ ͛H=Owz7*S@Z^sVImwb|% 37 6>1 l VCj6T:'Q-.]%/iLUJĠ5Fu0KYzR{d%\F2-KA]G ZtL0I%`ˤ$'KY9AcRr<@*J<"(u|3ׁu@0tג( nrUMg!QPaP\%_&FKNd%Q*)Tp`Pdbh?f'-Q)lx4,:pܱj뭸+~Gͯ+;$62 ´Vkfv+F.;+vknڮnmGP/+[/s@{AT́ dpk^; (@l$OLqDq,4o@w-9ՆO{2Y < `.?la%2[mm4^j-Sm omzUxe@Us 5@)(3rƾ5`Yg%?Ru6цzBb6@aW="=p & 0} ] db/taƔ]LP̈́'vJ? 6.f3^86 Ø!EБph(<@du`'5c*a,C?Z.a~4%Z7@qK@ǧ;*uk16`EeC=Pz%ʑ두BB+U_[<ٱ hgs; '!%: Iz,nKҮ3 36!W" Gxh\WD=.z)[WFŴkm4[ZPicMEˀ7_Jn&g6]xDt~j/# ")NӦ2XPR!!&,] @pHHHc`l:ШtJZجvzWH>znb!aLi ~ TuK~ KG|KqEKeZR{Y\\uuz&Ȁ| { XV \խULԥ{ $%EQ Wf\\2"hM w5 ]["4@2TG Y*MdFXp¶|P(چ4Hюz!<ER5@ ( ܣ+#F53THl=kT_q8mmϩP U AnNADB*3A:Vic(ߕg[ִN2ɻNpѶ6ϭi#D]@` 1?;[h}zVh1]Wc^|c%-A m]"sIW[lTUa-Us%TօHE+՝CUB-%X@ #^(H"ށ}^m |SSZ~M8ZHeTRyi"Y؏ -"JpRXO 4-T"hݖ"#~S$niGgl(XLl%d R,qDE#;Y::R!$A*o҄9$ile'&[F+fvۂ k覫k.,K- ,oF  pGl|"d^Ѐ(sn+뎃< >3@ 4$?j210. v,\۳M?}2(\@yAX .<P8T32tGHvٶR`8I?@vsta D]KG;:Q嗗۶jt4WAx P,3seLN4zN.nϹcD>_6ƎU,uAe@ϵ4ɼ [*cʪMB7.>huB[>oO, INJƥAzrc8m.{V m@(@"jˁd(Ѝڙr;v==@,n8C tNL_]0]-4 {,!;[@-!`y[,8\#Ѳ=.%%].U #5 񍬫nPQsB@lupVaca *SɅ:"▸QoIȶiLtef$$E|3qL@u > qKqk!i8*uPntG87Nrj0: ҘEL`(\>5s]Spi=kw-'AZ-l%l+Q>:+x+)Ā _̪TZ06 R kEֲa*!(#,N/ pH,r)@:ШtJZجvvKxL.tu!&DRM5[mQ-ꖉ!U!0cASo:e.~q& vnQYh_~yE*[g\1Klklz6cBVڰy-UVJ!skuib:f&/;f:jk @+G  xj+n8fy5漰w, ywANNӾ 'ו݂b3s/h| 3tq.9@ƟY4*jʬ*ӡ=SڌP6T1UOY X&M<$@]KPUOI%w蓂&3[:##v!r7bQWQ;0Tڹj;vp&!KFt5s8rgaB|_r5}ֽvJ4nq};(i<!%$]-% ' g /\>eo܄h_u*[gbvhp-hpRD7e}QL&"LGpAUW۱cU{HAw(w. aL&y%Beq;6Y)E!c}`5ٍ 6痄!7V`Ud(g"sgxL$)x`xx0Y&TB"@ŒQKʧU֜j@jJk%j֨hщDy-k 6r.ZB$~mAlZl2[Fe#`K辤_Ck, ' 7G,Wlg̰gTQ ,$l(+"0 Ȝq $0d;뼄0_UyPs7?03E']h\ABwݰ$܀ @q]VwW{3/L#3c}n#^9ė7r_0x>4ks.{Xuh BD˜@<1lvO;.χS`w=68CƒHOЄ0K]gnM؎z[-{#(}"`~V< ~ <#r)3]o'n>P(AUlAӆ$A)@+\X 7>@upAlozX%0MA`Wuht)`mLAmf&n3B6h=}f;b„9L.EL"yH-6t#ɃYeˀBbE~ ^@e,׸Op0 9L".`x=lA6|9 \$GՈ<2pzz3b,I5`0 QIV[Q%_%&3!q{z3\ H=" SӋNu>i?n.`dNb=vSm7̙`l趝{:ގCu=]f"Jak@txiKq\a,WT#F.ݱh=Uog żU^; +mONkV xU;M=>Ykac#9O\j?ȵ7j!A"ԯpW wl,5&Sc_#wbcՇ}*yÅ:Àt^h2dh1Gc^ 8Ӂ^$*__'+2]|m/\3:QQ`W[a4v[^F2&^_YWzf%csBt>'Q_w[-x`8w3[΄]& s%%u%*}'ugq\e5N#=! 7TQ$zyu>yIYwjm,:6ez@V&uesWv5Y~yXoshXxyg[X7FD7lWi=yw)e/%`8ajȁE1Ua81hMMDx y\r1X)؟9d1zZ-!#,p#@pH0G ql:ШtJZجvR!HwL.Z7EcMJ< Lq uVB`g x\LtF INWZ~Y{RCM`xL}P eV} k`nNXWTRB|K ɮcU}sr \BξFMBG c/~T ګhI gӳ~V:t@lNc FoZ, 30[r3VI{  UO{CDʛ8{0ݛOUhLq*70R/*W[` DIۢ$ blQcT1}mNzlj@@:P ,&qm ڲK^IK ]"LJQ8B[)jV lgc p1"!/^U[G{݌sğG/y}WzΜ0jԝf76OS`#j*F /wrxid]*I+J&э{>=ʏݛjՙ2'[栁V{%Jުvl\e( m̈ jlK]~9noz x1yN eme\x)۟Aкw`;XkA8fƊ$nkrGjti-@Xؽ-,o_c/DH֑skj hS*MrsgCc@1y4$;"uHa9Ѕ )0Qxű-Gqt%o! C7haYNj{Ǿ },>5 I 2+&F/@<ѱ#Nػдi0c׬B0Od~:( @fusl&iV+k6N3Ly)c62ߧ;n>NeC ;gcS_ZzA^d&m+67RAAⲴ_5vٺApq>Mˤu˸kSz=c~}@f~]6_ Iϻc4-_e]rc6c055EW1eŀ8Z%~r؁ "8$X&x(*,؂.0284X6x8:<؃>@B8DXFxHJL؄3!",y!0p(!Grl:ШtJZجvˍ"ȮxL.f|N<~X4o~JyF yz j DmFs XoDwdU Ht"h JqյQ ֏He _u솤vmEozR@δv@22I=)z#>Co a\j5WaPN\,ym,$$_+M&Ǜu)SzDzAE]BThO`Æd،Y6ji똳+]A6+zGJz.QD]B5Ps 0L2[]$HEuawСUf&b9-dzۨ|$, LZgkybiP3M|J<ٖ<7c޻J~5(38TcApDcuCV1-VTV256dJ3)֖.fk㦚 p7]ξ~j{&Opؾn0@Dw8w_#?|cxƇ(#>v#4c|]iL$ WmYnD^2\!:A+!('=k 4-6STGI߇R`\O#H3J e^ *bM]jFZ`TaRXUM5dpMAP~̂G[ŕB_W8 "3j1S@#KYUc7zlfn^MjWkY)vmXKږQM&R$J qEqDJ߷FsSrFvhE' Pj0`Y 4XE6բY2xxQ"lp"U!{3 0CoMx]< 5ݚ{݋P`f67Aqff8Nb<@EY8 zp,кJTJ2͵§m=2mLgRT*Af:_)1\Q%; \,HgE.bRI}%k^g #n]|-1k+ls|ְ5쵚#!y_J 1 f!p^a(2[ZW!JQBѩDc%Ao3uӤ^btEx΀#hQ&?UGk`ѯY CVG8غ$۩A|a#u>Ӟ6 qL ǮxBV|eB[S|:rGRLeK@n Mj,Hd7 'BAʗ 3Qu'ϽE+ !jB<-n 㤂n픚7V?ŔӮ tvA:5,ú'7HĖYKmTcS O,uyκCd g!q*79m|][M1}+gReUWQ!?*}zR;/WuoobOoUW !j#,r! pH,$Rl ШtJZجvzwL.t\0SMxA>`2,"qryRVrOI,. ,,/:BC56Jv- *p, 7,4BȐ21"$ YDG. 2* 2/ " C/̈w$@8~2ZԀ`AC H1 fF>22k V\et?Tj!  pPhô\ʔWq2r P14ʐ^ӯ`<'JP @5۷deݻWpR߿v׀È+^̸c:HL˘3k̹ȟCM4*S^kӰc˞MmϨY{֙p5P XQ!#RHb -J·\* dm@ɞs(p9&}i1{#^l,w_}qk '`zVVam~p"@bT!\O0.E|꬘aky1iY/1 h1ۦ)-zW+//ApmFF} d.Pl.ÞŧZĕb- W f.5MDQ&0WUFX z' JM^w@.yBp"Z :a{3'2'.0(?EtRE hpتX{$WدƐs1a=>fsi7N! 5$I6 ah! ye0ԨjDtAۜmLa?1 a5U&.S\Q0wIb !.,@@pH,r@PtJZجvz`.s -znc%}h0|zOJ}c| zJNwq ojJ dtx bOy~K sg NdUc☡Ǔ u|Ĝ_ eZ;kEeB0hތQ ֺ\Z5+ty. ʤxEg=/(S@Ap('Ej4WJFH]PybbkDaVwbGu;nS/8[RML 'b i P[kk&+vK+ 0 0ok{m \oWl1fyD+q7/*@nxL, | p+mbb\@AF,/Gg.5y|<5Q`5Kl LTG806g|}GAvA^`@v3@MߔSuAP܌jl]m y;Ǟck츫\).<ڰo~ߛ7/{˯هO,>oP}En֦hYbFfԤe ` |LZp>i_ֿYo[X0g o0 óp&Dij.,'ŭۡy nY6Et6(8p`2'7B 8XCiaCDHGյ.ZwcK7G:zuH r', *_l6nIrTВx?.,o$N*&-iJON ' ?ɌE$%^ R F-%Lmҕ"d&qlA4W$9⬅Pӟ_FfTY@A&KNG6Zb΄Fx{b~IЂr@8hъ٠z3^3m Օ";sZfBҖ$%IS!, pH,$l:ШtJZجTzxLrznSy+!^H|}H sgE |  y GiJ|C{\^LqP qpѽ} ~DIJ C DNGx HƐ!H !j,0D bt$T$ʑJDņ+e Lˆ=1E8j PNS#B5Zpp3 2\x `Up"ժ(C+VX,('wA 0r9ml[pPgL+ ./ac0mx ءm%-/GA^ip9tԳ&}w:R͒jtxxk;Hn(XWIU H@:`ى%"9̬g>vǪdj[Jg`bP"̤ה(e'D "Қ{>GҏhU>\@qBRe!2.3D1E3^.Is-Ztv *7rC [q'w viW&y(i]%'6jm63>LWQeC N(߇6ٿ<Ο'U{>%ӷOl&}ԵI6E {`D@ FfU"~)!Tؑ|v5O6v];KCcO/ixӆ {XARJ(').A./_+I LPβh\v\ ,./|kDz^`Ӻ#o9 |ܭRQk3Jd<lXpYSpm4< c'klRP9pt sL W.hu!IM8 n%h`~"1ټ^Nw| ,+C53,BkmB,ݝyIzK/&/q@Ǿ,p.}%'Õ6(G: A7Kh@Xj`Cw2:fh5ebZa|MPʲ~Ddc\$iI{YCN`-UXq{`Twd& TظM݁!C(mpXLg $QXj,%֖F코g/][*{5psTH @@Nk[8kMr%k˶),̩DU &`@VélhMHհUjJ׺]x^e׾bkJFM::6Wd+?\k*7bʺULmh%*KV- @uFZ3YgQrimXW\KZ}llJ6e 7[^E7q`-lǴ1su}'Ҷ!ʀWx |8-"->ETNCSt{˵2ܥ 0!Z[E"W˥WðĒAšnmnU3EM#cV :~W*Joۃ@;.nGFg-- v̘C! (V@{-YML7Miۡ+cvS^m5ൺZ5kZ`q!&؁6;<%ճl$x |FUDl# C髲~xaIc˴"q_%f^ZMMiP؛gg >C/:|-L5iP(QۨF%GlfQ: 8H`W'%.UC94xX=۰J\X%,aX[7OP%Mzw]ZiO6pO]l8IrUzP.]nKG*ݾ[MX:Tޅ.iJ&gٌeUX!U;Xmcey{T.,2-%*1,=8>^']:x LP'G4(&4)զT_pЦG^br@ʕ]xR}w򀀀?[Ǘ|D3XDKȏ;D ,ȳI r7_$baUDDz%.$((/&vѡ8e㏱Di1!JrL6PF)TViXF –\v`)dihl Q|4tix|駛p"ĄrALv}cJ蟔)閉@PiF0韁Dt0gP\9} 8P i nVkg=irډ*D <0kmY,bZ״z@AX`-Rk*]z*J"tnͨ^)z5ҒpBGW0S!#,( pH,ȣ`$ШtJZجvzLxL.,l~&ؤ,"Bs`|3/R3-oCMQ2. ,B,--Ftuz- * 0-rl "$׵] EɫʔDB"vF/ "8pt)\HT\ \@t]V !Ï 9=`X$f@Y9`6;& ɳT1ԩ0&糩S3 $=UOjuׯKVزh.Eȶ۷pʝ˶ݻx߿ LÈB+L得3kF8g8phg:Ͱh}xv Ўmٙ;{zSʼnw}cЈ>a\8jnة^ҵ޲4vDs'_}"$C (@_uV (hb- *niدRm0mk*՚veR#?lO"fkh+tk,"o~[\,i*L,bТM*kzY}$u*j@V76NVd^/'Ld beU!w೟,hq0 ~"B|^*HnE봭soepkj#y+tܯ½w*!w}^Žj _ ,gtu὿|a~clV'>¬y]w fTܽHkh™ __Kvtd>!`OhǜFp6U"R8 xG_VD NH 4[P3yMIDby9Hp_4e>̿8'6VqZ^ ?9"Jƍmr;/Dm>r1YH"ٖ E#'IIŐ_%CMzd&GI P}Deb! !,@pH, DD:ШtJZجvxL.zn|NZ0 M ^^ KFwqy0. IKJ ]B~^UKBy` u/[S_ KeC_LQˌELᒋDǼ1F &Rz~y @hF[B1"u;*P?EU#3HC ULq8I40*7 'gUɎ zY% l'}>+,%hS~Qtiۋ v__t*^B,!ޝ!u+nԾbZPe<ܖkkf;~ 'c gDӒ@hR򑍪&r4Zi ^P5MWpWVPg|ܐ6#_\< Q}$D$VN< v!f|($Xm,Qm1݌4֨M<@Ր/i䑇L8dVHF)O4X QHjd)$`fHf;rךtߏA9&08:iEp(&wp' ,'l@\9觠V#i~Y&*Fـ:64|~b+KhOd촟"dR@@+id4q tcvJzFR/'ܢ"%2Wi"b, pR,\ܦ(|8,yA8eys\p#.-UZA dPO 5J6 `S#6_0vL,m=-7Bs0W/qӭ݈;1e3@{Wn8g^kNQsӘt܅YC+wk'b?9*ͺ?*;g/ܷ콖Vo~?е3꯿1s\ß&GaH f'4eT Z tĀRfo2l$0aHÞr7cm(ໄLs $_:\|-IIȻzgkV>*tꎘCj; A}E H9%xwx82}q@L iH RQA! ,@` cJ'Dlp,t]P|@dAB-rDUQl>tQȅGhD:V=(htai6:tCe T G Z \mwCNM:~4FNm_o#B8TlIT.D,c+i[I9 _2 PŽ-iTmӕ})z,"y\ (PFG1Y}$l~ӤlH)aRa[D^!͸'x`p룓q\:s$SLKTJ,|%r1n` QJ )j鳈jH RѬ p;hL$ 8Ie> 7zT16EFkB;dZh+.JrUF\ F=sM߹iNȓ+_μ̑IGf: سk_>]7tX!wz 4,0@u| wvg\@\ed }hz`Dq rB faa!T.rV R@ Xav[I܇9hcV_ ]F֟(!QY%nz`AuH2~ ݍi%sY8jv]a(s9]dh+R襜>At*jtjꩨꪬ꫰*무j뭸뮼+k&6F+Vk f! , p(dHF ql:ШtJ>z` cyn}4 gii|ecyaLT eupdCEjw{RcGoYg Gvel{tBc rt˙ncmq Ýo ɉc p~ʇ&w106x0רgșWE|3oJ?vǵs$d`7زٍNXetNqsKIp&gQ q!Ka b3vFӜhA*򳤂ZG+ǐ# ƂKˊ9h̹ϠCwGDD^͚siҨcѡsޭا?_sx d {N\9k1Cf 9!Cȷƞv[s8 8^da0\u T7ݗVz gwAQuA}8ȁ| hbIǡfG^*ț+&L |ha~yr"!x$9PI~m`M:ch"b2Yf]ƚp^ihmE! ,z ` $)A)lp,t]2*|QN$UǩQ,Fɴ qЀRTQV[UMaWvA#oVhSEEC( " }:/fxDI9qk-c'Hfv -R}$[   cZpJ |Hc}|{WB}"'mdaa7[y 7 _)H1Z'3&s$Ӧ-y_ǂ + @QJ '#BwJnc:Kl Nv@)_*꙲cbxL 0ك @Jd Q8Ie ͘HEW.}N,2WQe81eJwb6ri9i`5a̹ǐĝCMӨS^ͺװc˞M۸sͻ Nx! ,l@p`@$#l:ШtJJuzؤh_h9M\+p q+ɂݬsgHFBysNFbHtvf|D~`F zYJ{yPH N{Cb G y{ƀNy қbL~ὫQ̰ɰv}ղD7zg9s#v)TE"B-  {j# 1!3si<A /Bظdՠ'%K%Pf5?r6Y`)a'XI2Q,Ly1YQo "2b|DLs3kv¡ϠCM:6s)ͺkШOGN=J6ᢃ#q(/$<坛?0$ A(w@%0W}ЉvAx`'AЁ)r!ș4h|e!yؙW'gТ=GcA Y`xiLD45Jy$'Z9cPj#JzY"@!Uمah䲀 7WzNТtO'*x{gxpuApI IܓP y>g)`f* ʁ=-AcҺ)jz+YZ+h:o`i+|h{y,+[pnG#gFmYhD9&#ٱ&j[ s6qvqomЯg fWRK[{/j'U8nӜ<+hf귀̦rZrBSlSY±xI'[f''(;#Kv1ѓN|0.2׭ͱ<h!r)Ĥǟa&gڙy9/.9!,j@` (gZlp,djxﳍ p($T:BB8lV*H@ v&j?y-:>\vEfmoI#y&F}% zj/b-q ũΦ˳ͪw Ĩ   թ.32x[vT: =Bl aMa@n\:j<@ JhL*Cz |ա 嘴aCT›jʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L! ,B ` `l/ztmy<0]Qa9E(*@ S*Sl.TY0iX<L~4/(|O #qPGCkF -o,+ oavë\a ʷJ Խ vW ՗ tp9s2׀%!Tia%I8l@v$;<&!C)nHYNupi=JI9mdB7ssX!, A ` $)Pl뾰qm@S qaOul>'%ΚMbqH`xJI212eFxpzs|H p[]e6k(O(7 %S{K|ij!!!,L @p Ȥrl:!H@>جvzx̌ hW|N͡c7BoUC 3~lpxiB ůB8X˦ͪАȤڧܿBBa+Q:oo)\Ȱ=  a$3jHVDUɓ(݅'eEH^tIIk\ְOr ѣ=$ӧP@իXj݊uׯ`ÊKٳhӪ]˶[ ]T ׻x^}˷߿ N;Wn$z H,UH<|1'0p- 0GsM@uW ܹ,[;6#[, [ӨֺhU܊Z=lG`AịmNtt#[ 8l5 AƘcŁ[q=q| WZ @XgTmg^}X[Á\Xǝ'|*}٭X RP[5#*~{e hd$" doq$k(AST!d5lyAP![Vph.h#H@B Wsq1Icr[fz"GvC{iWBkGniAX&nZYu)H(F+diꕦ&ޮW ܰnZZ-`dU4-q=`^A>A[0۫,0w"(ehzn r6f'bit݂jZUUHeYPXʛ쭂9˾j*,0xFpcvf4lլp.}:=yPJ+q#:3AG CpBF)d&ezSGyϏ@tl* Jw3Z4gj;AdO099%a^+6ֻU ~݌0}|PQc*>|θ/_e%:V_m@,͡[e܋/ʠBbc6:몬G]`2kxwx v3oj<<-e f)b[&磻}w]Y <G_z f BAy=s\Vu6yZ!8w9A qf3!~ipWUsȀ\ qA|Р=&U < Az4f(&N"'y%n )Nxc8]v]9ێcjHp. jGh(ۊ\)PUu`pSd])[uYeUYY dt[zgfx# 'd>UzfE[⦜ިCTROЀwjkj:|ᄇr{r+di{m5PZ0(Ul ?p=m,1dݷt8fPjpǖͱ}Zb] 2OLghҸ\1ՁU]R-ujxm@Ir؍KZX(ǕpX Am٧6n:DŒU"i33"WXݪ,7-. qYXZw0 VV?[̊h{G@HyRA/,]=[&` .,mz *dh?;es,:E:뾵,AQ pڣ֞ל3KՂs&8p%64J#Zy!q U*pGY'/ѫ < 4,)R6;c J=\mavnu V' LĐM@]6Ul0?TquU\qPg\m@Z1]H3I8"I& R8"&ᄷv#c@|?H@?jr}4GXCxkmxĈ3r#ėp\xiiJwe(cqɦ_LBkEňV܈: w<AA>$r&T$Q̶eZR_O:yj'1e ^e]`#:dȢg Elh[' 4\R[qF +Ui-& I1˭`Һ:޿ z=*֞F= _q(&J}+f5&tVSUJOe[kJ/HCtv6܊N@EXQbic?%y}-Ycy&1fnN'밃 瞸NA!#,< pH,Ȁ`$ШtJZجvLxL.-dӠ|Nomy{`2,,1B-"B3506IoNe. .,/2y7,6y0-"$ SzC3"zύ6 2/ " D, 0/Bx -56  J|,z6Ҁ~ Çu* -P.*8PdnS4ᑫ0e!WQeY2<[ca`gfBY!ɥ,Z\jq#3n[dU0()bnpfVM7yWn;\47]ʉ;oJly0kl' 4ua۶UcrvXR0ٹʋ/\̪j[o][KA߅0\6{d9?CXy_py^EK,]{3W^q2C|4咡֥j)3Pr]i7pP]rAdC!Ts҉z 47s90vp{*L. mUt+ʆetj*ܓ[V'S Рɒamj[[=%JVSz[N$tʀKSh8Bmf '}I]a|#J0줘-(ԐT! }%>%CPMP_Fh*J= G.BV482/A!! ,R@pH,Rpl:ШtJZجv]*xL.Fe|NP)zmB N (&{me  (R  j 7±  D(}Ӎ7D +t ')& eNxj?9, J$"ŋ9hȱǏ Cv\GI9"S\ɑ亓0ѰaM.Il:aЛ*snt T r`PHl5 H`ݸ`l֑["R 4 t(m.x0nڻZ]^Ld‡˘1 8̬fhW^ hB`1рA~ ۮlɴkݼ4pċ:<ΛjofHQ .8&򒚡U[֭DzxWOѱF M?}LPaԟ!GUKWZk %_zNtYflvt`@T 6_ Vb}6E܉3 s7=@:9HG6I+1)%5 1xeBeQE8_Upyg _Y)cSN8_u>9vATR#. f&*Gٞvz)ژnBzXRTbꩣ:̪)#$a! ,h9@p(Hrl:EdJVSVJحzJL`xDM%\P?hFa}k]!g{gffbniYgsulc[A!,oIp(HЀL*MgJ5!*eb(S Ŏ<]Feq_O}RDJlA!, @pH,Ȥrl:ШtJZج6z۰xL.zn|z~Xv]t`GO ΥxKd HZ."6|A4,8G;'~(rɓ(S\ɲ˗0cʜI͛8sӡ@i,(cЋ(EzԘMKzl@ODjϳhӪ]˶۷p5*t??r]^/yU0*ͱ)h(K˘3k̹.WX(A @'NNTA6&l1[%4k;KNة^jQb!"U}]S] D,]1ڍ_Qd! y=!xwyevs_eE!g_b$6P鉇 W-^xWVH-b8ڍ 6PF)eL㒟P7Xh'MPC`05S@h\yfoEH&t)|矀zYl&yEIŸeyfj 2> FcF5&jꩨSާn+UoWr Fa:5bTE zdgzzJE}} W`=(BK˘Ycqm[|&ZQ"rKfqޗY ͋cl_(:' K[$FoTc%RƋ y$lFr,0wn4l8|l7]8L6/oO}q:]*;[ݛǐ}pպa@||+.rGH'-n( 8! wF}XCw ĂcG<搇HLP#+HŘEE#b.ڬK H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz xK\z|Kͯ~LN;'L [ΰ7{ GL(Nu!w, pH,Ȥrl:ʓtJZOЬvzxL.znxJ~[ C  {S(uti,"+0/H,B2,KN2:Oy'(,>R05'7DZXF-J3+JP- +1O|'0-(32!-!))) HUi@X6#M Ƅ)KjACX.hX/]NC,3Q bTѣH*ME/NH抉C €2AU u8`uU*qzLE/TlHQ*UVw5x.ǐ#KlpT P ֭ZZrѻ|tIu Åa?УKQ-װy2qъ]J,hsC&h/cβ|$S\N1݁&G|g *QE!FYEB`v[x+z1 q>@` 4hcBZr R}S% -9J h }(`y[@ h p# Hf@:^k:n!3ժ|6AStA]IK:XlxeUFgdq$gcgqg"k~yFR"yb kF oxu꧖ZYmhE]Jo`%sV͌]r ,]N96Q&ҧTo2\jeX{C|TyKdm6N\=,D0cdp/mX=!,U @p(0 c`l:ШtJZجv%$wL.t;'|NSEb\>vkxobi% "B $  Hn P$"#"# H H{}vB%mz|~fB{`YжM#PIxn> GZ# *]%8ɚT$Nk"[U,fhŊxQ*(Qו#sn 'HQ-,W"FJ1.z]uc(DNqBh&|Q4ʞP˴:!,4wdo3L<9!!,] @pH@Fc`l:ШtJZجvǤK.|NۥD$4.IwVm} at$" #B NW$#|E  yoHGs~\#{GRz"KD`appGA*\ȰÇ#J$"*(3joǏ C i"FAx@ p$A \*)f͒ @v*4聆 d$Lhj*I'wЁ_O &R֪[8:xQ}IIKAPR68r҂ čUKԄ -}*ղ"tMISfkr*@lW0g烁 >3_9F+p4zzT `?b*ζ?XnuS#|YzUG-rYvŕ?`|M@W!GCE' vE2m`#ފEƘZ":XYARRap>OrЎxVvC"7_v*>0b..ENV]Z@O%?­ԏ Ɂ6Z_qxN&Te[9)&Wk^#?E@ft^=sK0VdH;Rj"dV~^9b'+fbjn)m偵=(̲YQ{d㵘.M9F6 VUn`giZL4V 尮 Q6<'I ][XKdl?\Vś,>*lKq!!,N pH,D:ШtJZجnxLnnp$"FC2S" D#xBzV#$$x %B%u"% "x$B w| %$Bzds%EĄIWDޗ¶Qڏxtg$ ÝthzY'Ms !v߶sH┉3j$qNJ?Iɓ(S\ɲ˗0cʜI͛8sɳ@ JѣH!B, ` dihlp,tmx|pH,l:ШtJZجvzxL.znp)N~gszuq˿Ӑۄ&LV 4ӏ*\ȰÇ#Jx‚ 4hQ >x S\ɲ˗0c \TQM)҄I*]T!bЩ%HxgT'2K쥞'9 s. jJ0('LUŨ2^_^.\%MMt΋ȘQ'+hmmANȓ+w@noBxvnG&Oc6O7o_WӿXG @(h`O t]P1@gEfbbEEx($hAOy7g>uAן@) Gp+CF)TVie]Leږ\v) W)dI^lp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/b觯/o HL:'H Z̠7z GH(L W0 gH!,NF` `&9*[‚Lb239`  `H -x@@p-Fc[})!,UB` b!(I:閰0M$41`O!c b<#rajYѦR.J!,]W` $)Z#PÊBN[  '>p蚃FDmdX|¦| YR Lݛ~kfM!!d,l0` dihlp,ϴIx|o ȤrCN̨tztmԬo -z+(t1vM O{}A8:W%JrRiH5\, #w $%& nw w "n Զn%#nޱֈU#AZ56M3 O) 8kΠ kfbT LJ\ɲ˗0c MfFl*)@?>EТ"t+**"V)׮Omt쩲fK;python-nubia-0.2.3/docs/non_interactive.png000066400000000000000000003641021434345131700207710ustar00rootroot00000000000000PNG  IHDRxqaT &iCCPICC ProfileHWXS>$!! %t)7QtETFH %ĄbG\ *" ņۢÂ.ls3Z G,FIbCSG`mJ11rA*< qÓ W, !K%! %4%PDz N@H2y10})Ď"Pq3ľ\q?#srr!V&86Z$93voɖ 0JHb5.TϊҢ!քYX G{t@%|P̦A|'z9_1{ҬAVԐD+ 9%!J"G:T%qD >L9(AɗN+ AX8/0v8;fkgfIŦ⼘x%7\+3.FȠ\ m I98@28 Zf$)FD _tx^b˰Uyu|Ō,L1K4-<Oٹk6TO6ڐL "C{=x}'<%: %?0gH9 V}u>0>3~0+~U6\^";Q.ٟl#u8SB+m[`}?-` ֌5&v k.bGxxmY0|Ik@FcagJ /7z~_ 4!E`&2 ńr!itD5~{L,RFb8K"I$R4C#֓:IUTULTUBTRTD***U\QyGV'[dy&y9y;|IhP)>xJ&eerrVUULSuPu:}gU~jR,$zzFYi)<2Z5$#AEgyy z= YR-@mZZKjdu+u:G}za 'h5i<$iZik44i| X8"jYk2JjihkjNԞ]}DCӱad,ٯsC糮n._wnz#z%zuz>3W77 L7dpڠ{%#c2fxѰ(HlQqj]& _jc&/f6s4Tfմʹ,Ь9<|yyEl;dKKZVVVIV[Y kllT\%zfnlڹ *.٣nB# #=GFV@upwqx8JgTĨQ ^2z_];utTΙ\|ͅ2ϥ11reF.rmq&qurpOut'3sg'/7<^{;xgy~>z,}|8>[}:|[|;L8~U~y;d x( <Ś:k'o~bR:+x!,7`J#SզrH%&NDs8iʴ.[}ҟgd BpufXYY;rTrRs4EYSƹ3rbq4ikH%;t1O d_~=ͯ8=q3D3.δd泂f᳸Zf^0ᜀ9["s3W4s~] ( YXXVna¦"E ^,){b%.K/Z+9_XZ^ڿNu`Yn7 o2ǫ"Wկf.Yn5ǔo^KY+[۱.b]z+ol^XQWiXF+7n6\[[CWYUo#nt{<~atǗbwvm{y Z#3iA{kjԕd^[x=hyPI=R?Aј~x&Ccgis#ˏR8Vpx[=9SO?}Lșm>uy ._txO?_rxrSWt5 ףHq椛xξNW_AտlUqaË}}IgSg&Ϫ;?o bΗ}iU3o|7]KoL9>|ǧIM'bk{9b -e0Qy6SW "?nq+&orkɽ pHYs%%IR$iTXtXML:com.adobe.xmp 204 1352 1 ey@IDATxw|^u& AI^$VHJX-;_ueI6N\&KU( @w|s )Qw};{̙sΜ93s ,~Ҝ0`x_w0"zć'c0> =Y 䣿d%0'Ǯ%~? ǵ0ץGYhBpAk`>ʫ0 /Fd &YޯnjʫpC\zfČPY\#2qFrNqn{ȋ?jp3 =5,:('3dX*q/U}Xޭ.S@zuц}(!P.:DEat 3*\7#oeA\ʫ ._7[N$wW`a-]:<#_c|]q _mx7 |`|b~*:@B(j4@8#o ^E]ehos,φQC8 _ qJʮ<|8Ez \~=fYoDO }>J0QXyq(څkpwe|JPLD`|YVohi|:z&W.! Ax>P݌fexW9Bo=|0=]{z78m`\^o+Gjz}{`,w.+ o%+F2? /*ԮGXR2aI>7LYɸT+>Tv'`sy,Fa*nHq"=}XriW\^?S_O>wqeǨ 1}_K>P=C*`t=\` \>`# _T|xwR$ÇLЪLrW^{ c2k?.X)8U+C0~A]/>{k0tCp5 FQ5wz=GO8]y\eO[ /[!\+*G `ހ wiDo= i; "a!<| „*+Bp'G`\V12TʣxFۛ9\T8=-S^㹦|ʧwB0 &I!*ˣWt-x)قf`Z+ 9H9~ JPy'd YY9xvx27g̨w }xUo.Ѱz`iyT{x?M >Q;vu*b'#>B`n|<HOOO\V^'B44!A\7v%dPp20^o;WXFr|=0pp7Mϗ \.Xߎ'+C1up~| й 28|=⯣G\S>wv(MAhz"x MW,[p. ?hxS?ጾs"+gS9Đ}.y>q;S3wpȡR8\VQ&0c\i>WT0xvY 'd gq1"|>=eh)()*82)98A9 R8u(=˂ r@ׁs{xF)-(+u E!z8=f2yExG 0x/8=++Sp֡ xC-A0kO=ߺL|>y*gF%y>0vy'u +1?\3Z~G_yBA?FO#`ڱκͣz"?P`c}K;ǥf gDDD>0X 1ɂ>L{z%t^rsDݓd v}fDr!Yx|PO:ק2@qH%9@>#P^Bp!S# @\úR ~qs07۽ݧ׷z`AECEhM ۷Odq꺼/=OIFʧ':NЅ=|2 = ].'n"qAʥ;Oз.x0'!+ƅ@=B|d!z8!\A8FwA_/7{XyF/)fR«]BJD{]!y3&#.#B`ļqx'Q8C瑲bޟ^$e[[;P׫sD!"7qW5:YEpmČx'y@qoE$?#aTEj%U^ "0(|}389Xq8 )]aFRFsg GOyD, .FdD3aD&o/~MAHxf!|;| P>jG EO^tt!|L*:ڨ2LAz.~ż+DGVJ?Wz7#}FĂ/.@s `p]="C3f}Bp+(_kG :ZK!Q oKp*?.ǰc%d!淢7"Itq:P!v8l2nǾ&sֲRY/_Kjc J gZ dҾ2*}ЗR M78OZeU"PoB>ux8JՍ?*+A(C0oW(i(#O@Y%Qۀ܎gbvi. 'ORB(LTy5a效j/^߇dqbJ# a߶z*AcM#h*! {uӍajM918]!k"ySOPx"K+9DdP?'_Zdώ^ڍx亊5Y~^(6\IsZ9k"܅ys`|ܿo7"5)v2<&=:77Ʊ螡 +H!ޯ}_V/V`dwcqmEXWe6|>ޫBS}<ݽ݈HÃli9CMhTyI DRgYPm໣%|~oYxMT&!Qr4bqE3>ZVW |NimKQҋq`^e ټ2:ztX*8ؠ|F0}_\$(9LpY@|' #, ];!- Om=d(5Acaս;K Oh# }5l67,Fdd^zhĒ%x\H}!@qQfb.˜%"&5;1Lzj97nQ)1`~TqǼ$NhDMM=3;FJק=#P \n#Aw&.xdmE yZ&^>g.RФH]%qfE<-}\ˊ%ĠFI{2zňDqt6n!X$E9\kN_h_cCj_6.Ô"$'Ƣ'}瑔B[+y rvrYg"/3ΝƯ@Bg@dq.Y JfLáO=Ca6tRp }8zLwvQޓr5L1g0XN +,#n+w\R3Է!+b݁tēVKaP=5|LXZD$GOO#m+XU[ET rÒTWI墧D#OeUdMT\MB$mK&ђD$L63)\l67#3,r31ܹCT>6YOt G%W9lOcIT.:ځ#jLe4u ۖ#  0:P='įpN@ r9Z{.mg~=-߅N0z1FV^>$awO;’R0[T~!`]D'?݀gr^(WŭAҋL2@\Qu(OPw`~ShiNG7FQ?J rX]x2a[ahr2R{ڒۚR=3)qcRݓThɬk푸< dr$pYdIkohEN@m`\`h~!ZVj*x#$kEXccZ`="9)n-⼮ E<ڬEj0[Ss,MuO>%`#oQ AJNw-0)nʐ;[cK;n3=?_@=oK? Wjj60^OrHQz74 .ă Fi \p >9{a ǚotD&b|q c.E|<֞HXU~f4/`!4 1E܉{|wR8Q,) ]4VV75& \pǧbKF:/pJ#摒hYo.0gELaq=͂cU%a=k1^\^CF| =fte1zǙ܃|!Zb@qC#p;g-B9 =i3Uv {<݊RiLde{ʽoV8Pzzr8$`- ~W:Ct ݁gW4h< 2ٰ=^Yqmͭ(1MGg=׌qXr5"^{jF-VO bhpC1UHlÉhìKOmd<ŻPuV+ [q%\dh{N"$c ?U_^RL-\ L[!WɦtzJk!Xة[9ًϳrmfq5PކԃrΧSX䭓ҎS ۏ_} B]Z gXe觼:T푊I@Y0|VpF*k41=>),ڻmqtQB։(+9Ɣ[g8Twtɴ-rY:Neդ/qkt$SNʛ2MFWI:fӋTƆG㼉"1HH4pEnKuBO!lh7ۂuP ;X?b^Dw4m==j@wdQ䰭eb:1shM|Oq$%a=P;&Z*#Lmnx) $D!Hhm/S^ !)W8HQ?;0B*kzYZX1'Z5Q$#)~ pIG-$/tw09g?W r EM jXdGF[-E.drNɫBuuSLb4b~.> ճ.ŕq9t$mPV f`A9%A~B!𮉀ʮ_{/8]XS<̤jlZ:/Jc5؄Pj u,CC"]xz]pm{80ce\H%YLɭ/XQZ9 wѵEMRɈ/W!idGJvZHztOJ*bXlXߪaP9HEѲ-ܯPjra\o(C: Re~d{Lژ.Բ4Kl+E]9NKegr(XZ=0Q5^mHTE|!D%&--l5~^m6Q8kϚ~Mɣsj;KSZ*hMLG3EOM'hۃ A }N}XCʾr6c$[k>Vz09R.1lcDg^؁GY')%Q׬q+@jgU"?rkӢtc49~ uv*E;z{1C} o+l+9R@#Y6f*6Dғ"IxQ0΋67 zIuԱlDZjh5 SHFzHaX;  e`oY)*5Z[ljXSFAtJJ/*qIlC9X>w 23؄cmF"2 xsjGĬY q_|4s\Z|fgl9Hl4ߐA[P]Ce`ȴ샮mFFF 7`{w8,x?p!z81\~}ySƓ0;0E8zc b&Lôgq<=p †xLT$mziN)V䱹-lmc'p79DaXs\*A=~/DNNĬcJh$ *8!egN",-al* `Ć]8u(/S^@g KsNu}~Cׂ[b'ôдnWQ8^ U`qkoƞC[Fm}.?308D vО=xud<66-J%q6^s~TVlE3)a;u/ TR[& R nG`.u,>F ArnOn؋x f9i/KFGOr'"6ٷ;/Q3p1;oLȊ@'-[F<X'b rh*ˏٝMFaɼ9HER8m{1K@Zx^ߺ ylZA."p &Ag3v؉梩 1\Fb(R0Rk6߳Etv̱#z59ckۂ&}$fcB*1 Z0utLK^Xq$kns7BL'-[ k{є??&8~Ad}΅w߁yv8~epB0OṷO P y$Nj{qw9ؼu p:2w޹ؾ VΣeƒ'b\8} :~@E<,~!=5!}].hq碵 ͙s`% sEhAwʙ3gb%fIc?[~ /s܍Ī%lg4me#"#1]G3ؔtjd'5Ic߉s"6Qa^<0㯖58Y՟b/QAI!&Sm+Ce_ujMP,nvm7N>7UY]sNҍ7-d]U6$>"0gmgXQNP0}t,O9a{wΫڨ2QClz6~t{1d|tuQ݋cMCHDz٪9tC\lj*{$ʠ $rfnPsIK@Oѽ]_ `V-:Ο>vR$'!C1ظvPaA}H^.>Zzlr̘P% sl;Ȝdl-ĥQ9np+w/=<#:5vl^}-Obx2:!>u46lވL r*-#p,~ 7v`g>l{c;Nބ5Ikb<-t"/($6N"=GbzDf[M{A"= p`^D=\ ǐ 1<yԸEE\0-[U2D s x>,eLK!Mi=.pT!;~X[6wdgNڏ;PuJl{;(--g b_gk;|߮>](Qi X4ރ0#;c0ݼ}`uX6{6S)X`& QV̉kpd@>Qi^nE~mm\wѨa E :#Pv!\ N0W&=M,=?{N-> ү;tRCowWј4}b8]7QIH";&u_ץ/SˉQDWghV Gxd}=L)Xa-e2sm&M.Cx_-~vHcM1YP,Xb6' łR9q \-T Ϙ1*ʏ>4uyu=꺓V;¨X*`|rSBuF$.< [n{1cR,3 ]B kP6.MH^r+>f*g{UB6)ws;;+y(" :IG-zI9iE |Suh,5t`il!&)SȺ4673݂ S襥s޲xpDt5v]a]}^8mny)v[MY3Ksbl} 81mljkeB1 bԹG#rOoOʽrAy , Cj)EE]UZ6IH4=Ёֺ|p*wji,]JR{1 w.ׯGw:^X#C©cˑ]4Kh4(HAl!43Ɣ֥.)K#J?\IYxhJdƇKCDZRNn'/ƻ6aLJtRǏl#w:f -m}XrV/&t̜T.l7v!ڐ'6o:KX=0(}>= seU]k'3 ؽX6w-;CL m-QOtF`5xhd,̦0&cTI.8>M~XçQiۤ"Fs3״jt6?sb_5 Ük^bʋ1Y}wEd`x_O3Nc^ `U?z_z>.h8\|h\Z=xWvO3xѳD˜ˣ:ܵ]O12vL?)KA@y oFB)W z,V4HyQ[8b1]u9#r?{WXwYfN;(AP-dug>N(5N){e'cϸ DNދWϞB ~kX:i"铒AP*?&IJvoZ~<\Z}uwլ l^2kooNr.܇_,Gc{ێoÜ\)5}9W:Ղo㎯%__?ާ7Zn:ۏ;G߆EKg!b& ~$_OvZ}?cTsg2\7g~ |p5=?wl@Gu3nbp\4q~̜7+x}'ǘ$vM!g =#"M_lʻWrO Ĭ q5U_/~Xq l/o D\p>Fq?]l,iۣFL2>H-7Lޭm2Ym72Ǯ0O8'=H̳\ zzc7?OY%y' gx|RlY5Om><'q)Ȝ9V!j7^%']xAG 1 # \^LQ}G/ ͥ7F$b9 eP1 <8JJĩ7j (΃,B.:>I=Ebx=LSZgSXN_ofz;70g|eߞ<ƓBNޓJ o>e*zW8xD#r@ I?dd Y4'1\ V~_zhn*'UV:7r o:JMH[ϲP1X5cw_IK'~'q:f4/>'DhX~՟|?\lCgt\).E]˻hEڋʥ '63l=ܥHI+R5M?]pP:&H.1qW:xKWv8w:U7҇zs(uhB&dl塝*Z+,r2e]!\3˵tq;;9%SV|襸iqkdm1{i-ZDђov74n- o{3'l+ ϼC4h`7T,ĩw|;&_9l ԉEhǚRo;Le] 3smC8]z<- * nzRν]8gOFSe.Ub҂|^Hd(݈:y!Upѕx5V^Eke]KnT^df&[@Anb8dq;6OD'LA0'-%ougߞٍ)9D˅NW+p B.) TTᜨesSiui6Fl$Y`M&ΜuhmT.@S5&y*ܦROZ*e9 chQ._w"iˡpM#@!TVI5ic_[>ʼnɎC'Q~.Zc0c8tqz ޖDk& DR oQy/w&* yi;[=s"}3E^a2 Ռ#gs9y=صn59qgc_};x_ĩ!+,/pOՁ]tw̾jΞzkS /VݶVTWSxCY%hX:?6Xq"r*iD=QVhs\c Ggиl h@*U8kx w=.8E]"l\ Ou&].r,v?[@9yo0^ ,K&]/ηS5qWYhRO.۩XH#LQ(.X `}o!f D #0 &6tBwrA[ 9^3&<4.DQ)wCS<̵Tt¢Fp0"u]#l6QJr'|/~f<CuR+'cQ} WEKؾ GL<@IDATJ^G$Lk 9~=\.n4=Nv=zFGf=ᦫE 057ke,Rc8~䇨A F^̅M%0NtzAa|~˭h uC/T5$M;4.'0twDeG@Ehq} Iܒe9ر5y+*IubbIf*V%v;R`ȔY#M׊Gy 8wΞAX >PBnɲSkW=ƻc(Ou^2%c8ӭ-RV]I;gx@3*_@(\s9 \,ڌǁ:v|D NO/؀E~x"雮xxX[<^.4ȨA '`QdL;ʉ0'wzQ\Z89F ?-2N94~&_up>_,(B h>Y+ Pſ QZ;F}|^ N2VP6]TA8.f;XvYE%KcdGk:DMyrO}7ɗ-N:Vſ$Mtu)|:Y4c3YX{'MDoLܙNjeW8qҍ/铊hmKƲ1yb9~[Nb6Y])?+l`OJTA? FwwυM:B]q6?c;.y> J>laC'XP9$SsdŴ0iR){%8>j!edu_jq\A62 PΛx@υr|fNr`A:Gb0:.X.яFs=g1Wu'2?auj eLA[L@;9SW&.9UWf1~cu쐸 y2믪G{}hUE'Lj?gH%-3GxSY'ī]V!L krJMB2jES?i2;~v}3ښտ-Yy|i>:6cGĴ]PAeAeKQ#I$ =Z[Ef),e8'*a,|/c0.Y1]vFKNOz4Y>馣quXn.tCwX.ȹwXށajW҉Yu]cTF)Y=9$1F2XkK+y8ټeEpE$d&2wE(UQ 4,po̹gwjÿ35 cz'/ S֞TGӏni\sWo2mԌ fIهI]~4VҨ| ͚Z8 XØ}Sܑ-:߼zs(z|S{%:ߩZS䮧} V6aS7ĥM}h Hӫ(4tHȩ 5Aޥ ¹ki7܏06*`8}=#viDƛ`6yi㏹?~;߻hq6 ,}Rie%SlO<ǃo[7_E_~*@7Km_zuƓO)X|Ȋhſl<[@h>sfF8j_\f~fTƬ)c'aYFW CՑt\SeVPV@XFۯ8m^k09y"[*Z^%s1gNuE]Ssb#~1Zy+qy0=%=2&([3[n+v#.Ey[X/^irTtNn9MJl De(Ȧ)y#{?q? `a¬zN^t=9xŭ<ԇ:F_F왘46/"<õ[)xO郸}m838P=rrÌ;Zj> 6"5c4 Li+ڽdȃ ;<#U_; Kjk\O•`=\>z'Ӌdz<ȳ~kTN-d/F;8۪&#JAF7 q_`ѯ9ŘQo_&(V_L5Y 1F-׫Nx}K h\oöpSGAzk] Sgc^A4%XNIfvnnۅ_zð鎐,rs5B78"yǔ%h? gRl~/>k?|!c|h<ܥnożI8w'^.=H9iʰtݭ p/!3;9,o↞MTݘ0i hw 0sl>a>O?ʿyRSM@q3"<BiY}2,w͝g 3LϛBw8:u6̟gCn˸S; q1C%BNŭ3籮Gi[Nc}(L\k۞|Ջ6?HI!f)3ƀ[VH,gg45Y^E_OP"{9\i_F5]|ق"s nO8v[xe*vp, K$Dp;E#DrfgZ/5^1hTl'hm),n(劰|;8U\-siٴ<yNLDX|.g[G `TVh'Q"`nq\hɬ+W +.e!s,`/_9(/aHA.X(4%LMEP]͛)x+WßyN&OPn O+p Sp2#6NbyR: s 52~j12ǣщ4/+rVH9}/|R2E`},:ȉN).⍷ ֲ{tD|j.00*NUqL}i(?L>iAG^5a)< 5 lQ 码.B;mT{sm~ݹh]7_F?ҥjL=hjr%3aѪE"$mL{;x0pdl60R`[d.qb:O@ΜK˫8%xqsυsyc_\.Sqcv"ohd"_?ҏ.NQA͉眞BOuZ2,_P?ʨB w |Uv8.j<ݏ7,_p` rQlfXǃ>8c3A('{΃x%ضg/&ݎ?Y8eQ_][E|Ga֊i-Ep,I<l*ҕQQT& xK *G cBs! E w7-ͦѸ y ؇+?~gB}(Ǭqؾr\(1w9` kx-ڷ_˫YZ)JY3xC*-?Ytrq=r>t c('4Hy5 ]q[8iMOIgJcxcq}Yh[8RVx.:bQFx4}un8-#NrpSO|^UDtV޷ā:C/}u[_[=N_ld.ڇx^b1p"Hkg;}yz*)=hm>prJ}D&} 7kvS\tQ]iZG"!؆y.$'܆P9$'D8~"9YWO >.0hKI˺J:YCR7_m_~τUCnBC6܎GEXۿ^]VZ{Ɵ[ڪ]({b۸j*bFg^x)e3qݳ xKd\̘R.Ճ}wxez3 ȫrs~JWq$KNk/n=j'bKг͆p~gU|vZQs4tv5*&lj9J?KW>wa V2m*;]ٛj-[WNUCvpr'\<&Pnk& UC'mh1iG{v YF[Men>z^V٦7MC %`D3fX-'1(-HHTc[nҝ`5 !LWBV9T1ɐqAڏ3Mb'/cO֠YAYÆQ'prh83)Ji(@ e~wnfX m>Bel'ڛ>]@uMcѭmWp~ukj7ōl&`eJu݃I:;E.|o}H9ƯƎ5[ w"frKY:Jbo g}:1L>S[j+s29G FQvZuF;[w{3M9(}n")Ǐ۹a%{6h{&9`z^&QLZ2Ntl :~ nJɷAƲdI>_AQS6'ʮ21;7@eVա×k9T "~P;-Er=~;YdȧB2\vtcv"[PMMp6433yW8h9`YS_; zqE&1IF*+iu *=ٔ4 Y)Ŏ>c%>lε=>aȿ(6o|Ilu3qg۪30I/51V&*CH;]^/Kh?#?=Շ8U~J]h8E~&[Y`E=/ysr|>}=twXmA '?i,Td0J4ZݜڼWO~rkп9 Cl\.nePl9oq1zv=<6|ᇶ\Q#5Kcꪾ$`kLd :xHW/Fp. _}$`å=wC(=L c{O|Gx1\qK~{\.~]Mk%d ֳՕ^g3`-dk&mH]9`+h|, :wkȔK P8%`=;A`!,L[ w6J*-4|p:q[^ Q2RiлO5 ? Ispp=/@:nNTv={Ky%ӗᶴ rGgU4]=7p] sS(xrmvxn|`RSȉv64ss. 88L:."[ t!&vie!V}  C!pw|&1JVߝ1Kv@m b2ẕ+luL6 CRҭC 5g_t7Α_ 4ȴ)R-F[lZau* ʼn-S:AMZ(Q| ,ڑG_BlGedGW{C"OtPhE@jkr,;R!0Ƕɇy22&KgӇ)YLOFf7^fcYz>9k]SIK&-_^;† 䔥<@?^yj#/M:F3H_'"kmEl4~5<*3iggY3"^g8]vδ!a0;** ^| Jw:{ZCTk_hp|"\eЍvlvFNogBZT+տr99Wn-՟ة?۪A=OcP[K/9'ק3FPgT4C>2%'ʧGl"M""+s~+.@.a*B=o-c12s?×PdǏ ҕ2*>`ÓuAePq+`pe¨f˵X>j|á]尟Js E]WKlWybZ! 46u.qGGU&XE"/] m@Cj,~mRQ g _cڞ^/Y@Ak}.2з]NLהU :xrץe_8>y3q4|9B"V>^/J% W񕽐 FȕnS,O7@nP=RRFYd ә)KuF0:j4#S<_ʔxBL/NeO RKk`$@'x@ 7S SVr G LxPu g<|u[~^7!0Gxf*ϒCI\>Rp7ԡmgdמ1:!DL:NxRb#!NnYb`sT̅8+' d@ڡy#k*AܠlVq*#~ uK0:v nMMݙ{uVV2Ɛ>*BT9TKzxU:zY JkrvėGXsp=/cP0~W߱Ӆ@C iG ߕ M4Yp<30Cg6DvI݀ x}3$E$$G/ MF ӵ_w6GkT.0@&Jkx T5bO=(S֎TVn͞]f5KX/ȚЩMQ}d5U:9G N;7ʈӲ0i4DľM:}a]t\^#Gꥯ>Y*/3S|\BީQ\:(gD[Ã>t˞ Jqx{ [ cU~ʋʕ=?^6=z=Ťd$r GB1.)M.`tM3d9|#EGWPۦGwFFƑq \7F/0@Ќ%g'uO,oO,H,` ;ޞ v :6 4ӋChyFB".~uMz)rٍ4Vu䚲8(R9Noh:rxx#+NϿ{'+y0.U4pMzXH_Hxjq]\&A8?A1CA|04^9t OQC-jKxD?o' _ax8\;C#Ӌ0O1Gqz>+HpE[>Q|X}DP}aAh\O^~dy8r=xO<=X(~\hL#5"xaċs׀"|[.e& pBz~]O9{ȘLqM! )d =Ex:aWb"x((sQBc聗խ*ځW(xߧa©IVkF(%u (Qyxp/jٺ8W=p|*D' w=lGO/ T{|"'R9Ͷ6[[:ځ m.4Mp]a$wGxB@,' pN >%]&ypK'7D\Sp%#!ϋ=&KQ 8p%9xxғgFh LVxiyMĥ{buraU0iPNv,=voJjR;!X"ԬcX%Аߧq|9|M>w~+3ƃ$14U"==ퟕcӡ/K.t ,GEuW"`~fJǷ-$"uF)3ЫDfLb}BbO S'н'ʅ0>sݪ!]ȸWNo  D!Ӈq߯=L5 aB;n_.ƳEZ"1i@F"oGy \wWA#!=>˛('{FBHBcT|Ǟ.bפ'?筟#xx՞! E ?hbj`>"y^| 8h0Bqi T^FU<?^Jʤ>~"/AXޕ>H.2>^#}(;xDؐK'?(rG ;Fp*\]"#z'ab~gGQa`D6=JQܐHDO]VsByq,&-?>jtˑ&cJ5ɭn'g`T5Jd+SaI}`7Q+tq-tFOaAz&+vǥ*>qxe8!=Uȫ=!<ǦG|zϻ%:|A[1h\OρE]H!=D.>=E ?@!q"FBE Ƌ  \#  ECTѷ½. %q'SxIw²R\pҷ^'0 b0DO aqp@a3#_>v0zy19zxz#xK0jq܂~5c} ./# 酴^c=ȱo\YK?#B~<z?/^Fy-zQioG0 D?_OF\8? |O@yFq/S)_(|j'DzE"qw= FcpJE􃠈3S$>#+[zXka 8{z8^O_O}/#-XrG8sa>r1=?9=Er=="W<ӋYeӮtqSxG-yz3q~1&?p \_xlj=Zm៑Cy`HO~th0J_q'翅Zb0?C>b#t8MTZ'W v%h ._q\^xxtTȉ tkDvpʈi(~xjEd1FFFOE^W pLprCB<8\{ԎgwP a1 D\bƧoB%q;zT(g:>GO#N8uxyB`xӿWeE0<o/G*%o'(q`ϗw/[0Eu1kyhˈVzuHy#:pӋ 0џ}Tq %o{".SBYz/cOHSPȉ^WW0P|.!gҩ9pHOMO D<A`AiLH"-=^G$d4 Fp ^jU%G?c\C>`x‡0B \.pȻ@O\C=rz9N>gmWPAܘ1J|-}nzcyhɩ2N[Bj~|n} Eз}xWe u_<^".qp< wp<}7?q<.aB.(M'} T^.I!$Ub6'qM T袜!N2O z

hCȣ,W;8q'@T8' cCz` č~uaBBpν /@ 0 'm󖍅U!SL8ݾ{m\O[dΜ`ǥ8Ⱓ'˞}.۸k7,çlPOC>GӤ#Ң  wCѷ<]0?.,HeٗXgo: ௏As:ۊ^p^"@쎍5l֟k_ƒ6Vs?{7#{<qzT~y еӃ̣mɼYd"[8% - Ӏw!dko /Ů,Y @ z|N/W^_wk0&+nv67@IDATK]Cߋ}^ ` G%M~dai2S<&2]}z[r^bEؿiD?c<:߶N{sڅ#6c].^x4m)Zk v\Np@GF_q}nc<;b]E8_gh," b=7;!ni˂$3Ex=jǗ*;$BHxA'sB=3ӋG 4FV!.TO$uOm̶~NR8 VZdV)$jpy5C\ن2r^9tYHt}\6VpoWVd{A`mu.|Ң9p4ph5>Ap u}yŇXݱz,D4 l8' ^b4Tė axA\=#Z%juGqiuwVڵu)7{C$a`w`z{,|*1(;v%SOjSVJ/ <׊ls8 w;m.TW[c-v,EX `4yhӕ|2V֒I yUDJEeGΕV)>ٹ@ŗҟ»h+Sݖrۆ{l"F|jA V0Rg?z툕Lź0BwGGGǏq(ӈc +XM.2< yT\rM7A:DeFԷ n"6~e5ڻ̒ɖ1!&9f6NLdZ{O=CcROwtڡ]U lޜr[b,f#dLZ;X/˰V͞ki0u1,t͆ɼ>.fO?rM,|aA8M2TF(}T[<aV \Y+@V9dt c;vU8hi=9j|AsC0VeKXHuc{?ollCt-eHF䆱퍗mExcG;am|M\bl(_g[2|e(3I렭C*'Is}5Ok$ngo їzX8®6FI|+ʧ1AN=$Cmܩ]yQ|?y59;6ẂWar!—j}Ww:[6czALYSyP|9wpp^9 >Gӿ w5>@o@юeˮ):-Yt%N푕l3,}kylbRa6"ݮ| E+ƪ:BAԷǾmqX7Њ ڼ_X$GEvU8)+(ɊYᒋefP(8a1y:|Ꮕ9"7.+h-].E0?>.p|ߌs [++ؔ Vu}f3r v1HG&Kl"1ͷlr?,oI&RLM?nmX/aZa y,Tc_~,iv30¶ 0Uk4~uHpۆP= ܖ꫓K|C0IfV4.UFۦjƃdvXm&:|Xɸ;2oX0iE(tgkG[~{Qgu f[+FXF^L D{E9i*#<j|:Qqe=UՁLj[l ؄fۺ%2G歍ȡyb\Nw|2֏)l6p .djVDrRW_mV(^ wf"f2A$xzQ`RVwFCA[dV;k ]y'lCu;ϕxTTdm[2rql6?W"+p3<J/0-ʪe'ۡ^yvVS;p,޻&eݭ l{73o>V:i%u]!`RȄ@>x--+*˦3kêl˷`Ȝ?q* 1H:M4grR7r +;a;֯n߸k)gNpq[Kh~]eGt^&+y\F6ԁ6_̹lQ{v=oM+WcL{ aܸ&4>bgcum8h{XB.>Yn8m5w}' 'YuQ:bO^ʁbwd%[$+ U*KJs/a`Pe]&j;x)=I s4k+)np݅vT&PeMK!qRA],U|ځ~,ϱ3Kd `(VXt{l-onkkW//_' JvB>hw w ZyNvY}WZsݬng]UȀho`PEl r}9Yb[_}>d'tвj.Þȫ>J}%K681?2.:</`]]8. L,mIaY aLƛV74Cv-TސMG/' <ˆ,4w acY.Vާ:u>jY+E&6/ \b;`)tRv:&;4yʚ&Zh I>*"V01!C\22 ]'Lv&mXE~rۏFC*$M-菃|Ȧ%Q;g7xPGR,vĊ>fp7cdeGwiVen_4Vʻ@iqcw*M jbNՄW^GCaI^?<J.&Y;2~K>O{?ȸjOX7Y1k3L5\Ę'9zhﴑR[ vΏߥf?E^&DA?Rn&#n+ŲOpMzjNF]`R!nILʵVX?9`xFM}(?F25~ѻ`p!XPC|@0/y):rq9z0c^5xz|קSOxCo>"== a\\/BċSY C)0N%}# N+ zSsѧ7ڜiE{֚*Uw|-; 0DI㔒nmm N570e,@g5tE:}CXJa l--s;˗ے9[\͖ru>CV֔,{;mBa2#Oeƾ6kK * &;3&ӿ&2YDu۬lMbӅJ{cOWˎeP 5,9*>Aj[OeyFvjM{=Ү[hGg$Q[u_m%3Vۼ3 DީlKw߾F̴4eY\_zhpؒ|v +r S01LFkɞÓ4,rlw]7[hy,O͵K䳶pN<]ډn, 7V00J C LY2a|ٍ6\r9{{n7}]6{~m?y=l.ۿ)#mFq9nSvqq/W/6vۼLySfs?6[Y);iL̒/Y*=oKdNe1b\o f!T;ѩZ&݁%r'+v38b;6YF%!VWmŒ ~{g}T )sl\k-vlBk69vAx~_6ѡ{lglҌ9}r]=Vz%c9m|&c~[wUuC'Zc+ h&ۙ+i{nK4l[t-'WV\TM˶wy^-NlMܰ1EOlK.bj Q w? ~ikYz6Zc/謓c2ƿoYSlqpKU߯~7mA>Bk*(^|U;̀p6vgu}'2@y$}CYSKrWWUmMVm^);Tbٴf;ٚ'럙Eţٞ{u! AE KcM!Noh͟` ԓ _l"96۱cG3M.^[>;n)dIs-{BD_z 2,dkS7i"Bx\1*56fo%۱c{f^`w?~ač8+nXiiwU2S]OO̙j#hF:޻ljfLD2w;h.Cޤ v㍫lyTf+t=d?v1-;K!P:#"9m wig& SZvE{OXJ2 ةlv+bWWX2~;w~ R{t- ^xʾOoۢoKe[ZI,0^Nt^|T/] .A Lc+V2]^bgDڳ>8to%%=.MP"ȩىJ-Zn:1~~H^cv ͖E}LOβ/[rލ'jۙXkU[!bWKlt;[@fҾ'ةYn'셭1ad0E1IZ_nw.booȪ96xm~x͠,!n4^I?QSw^:Lt%C;ف[lv+^z3'P-8ޝ_l݋CϽ x8cxƽ+5xw+1\,+4\/H if\~Y% %1O Y0/ d& W+o͜hէO 6c £mZlíJ;z"dau^_qmu5հ{U`܈N7{DjJs%̚iZlF٨[޴ڒ{[9˝<:d{rӝvݜvAY-rs5%K dw[XDg-A{xom:X=6.gdܓdk|emh4DZXTV,^Tϵ VyeζǟlG\mn¨ć.Ff-^E>fYÝvi(l&.r≿z^r; =J>EvVr.Ͱm?m0 _ϭ[dx^}̟egO8~1emo-D.} s{k{6_e&%9fgC]J΢a$u{ /o}2?z % B;m 쏿Qdg~ iib Mu+ZRo/0;F=-g+ {4mU4:Mwn=uZ?..Z` +*^V{ 3)y5vgeœ. ηն{_Ȯ[u}W۬piӔ`rbW&-fNtvﶼEړ-#Mi9Ro+W/?G/d8.G;QCY]6/ٗeٮ-v-22%KO߷o>{=Xr|k;_i+^<-Dvsen7^x Փmv;V+)]Uބc}mسocv'z&'ڞj/m6̻^U4:ZTy&*#kyseAtv7*ϜVi6?jGZ5LAAM \ʸ^api럿z;V^ߗVbܷ&=?õÔ,5O^.&ElzOyP,pgf.Q[zGf&~.G:gs9U᱗]d&SS0;D,=3O$pxIE&PU`Kf݃G" 5Man5K߱*&y:wXq|kL~*?J )\J3&oodRu>t%ͮyf;RNF<}碉7[ ؋0nGcwL2}˦,6L"1y߈TksmjbfQцmr|kL! TڲBT|m23l' [ sG,iKc|ǂPSl-1X99ۺZxJ'$] %:vV+m9tS+8*~5jaEV6Ъ7Yy*Sl:[.RT~`njFhQ56چ;6qt@ TϺIΟbrqȊ+bZ"ilc;j9WzmҼyvs7NIcqH![5xdKa`~f$م} ־rYFaJ ܹ@K!2mn1Lkxg@#$.RɷlmݮZ ̏ VXe]ms{ё)L:uЮ5{] XoG]Z,=cⴱջ[Pef;yղ<*=iItkbgET {ME⥞\]\쓝,eb+"ӵA! %MDQG鴂% N4r̄]fEesB eھ3Ͷq38K@__S̎m÷ؕhf Xɛ ޑA/UΓٵ+XxM3ER&~S*lLTYБE䀹P6EV QYhϳn^omoۑ.$SCoVYnK<+ #sD)äW#Mv멭%o \9>we[geEK.r6hHq8I3ꐖ=XsuXf"o/Ɠ?f/eɈǝ F*9r63k^&k|t&8\ Tk3gVbZa 'ٛ{љG]s]T򢿇:c}6 sogbn YS"jD[4u+0FN;jWB;C,wߩYWveV/;3?GۺKhR]-v؛֭-{9;vT'j}Lsgֵw͏m<;:Dҡ)G>j0%VWmwܸubVYQ9D#'m&;Wm=_{f3'y\ag^?ĦhwPݒ=loi;ibAB*%t9cUQ z5$GQKZmǮ}?#*LYG]]gލst=g8?6+goP{NK.;Pܠur "TUR*,`}g059?^8N(cdytEg.>LMQ1q Q.w8Dfclg.LYuyYVJ9Xhmuwo]aa+m'{8#OVG8Ȍ Ab]+ ҙHBW0=^UkVo/[ {w7׿2ipƎa'1Z[q<#u#m%l e$i`0s -%l&+jPHS4.l=df_h*(+j57ۃ?b3 #v@m}Wq1B<!Kzul0vWp5uph"h> 뀴DnpA879!;`@b@EU [Oe nJ># Wl@ " Å3dh-ʣǘ[+(HidH Ҙ"N^NZ?B,oFGVVXY:w߰Nq;:[jCwS<5`Pb(zZ퓚u:(4ECk kv,O~]f \eI l2u^|-3vӿԞPѰvhB;OI곭tTNN._139# G{7fN1bWT_q;k\OݬEMt-{s}:k{8?{ެn[-W^l !)$7MzBq۲$KnUW+iVUזsg_!y̙S9}L&OW1?og^`wr[e¶mlqݟ~lx{}'eKu#zGAMe2OVw@V; _Yg nزW5jcGdpviSlرĕ^qB):NEu0uGWa4 jn;b± =Pq Dp^^#l 62օ3#M5/cdjg=0/f2ϭ_9g6=?4`,V/3 ۇ|-hQ?t]l豒0/?9g>pq\ 8leǽx'"7+dq/U-`l9V}^ZSpHi`z/I]TP}=ܦ5xhamm@䭒DgGQ/Uט`v#6up`/_zt<4o ?C=3KS63dU-V+] ,rkYݶ֏3gu bN"oЄ)'=UGl{[N'wݸŖq~䣟0hꓽ~hByyݓLS)[A-`"?V}Cb؃ПhFOzkEJAcf%_ɄBs#Zض_-_lp%qT}6 +8KỔ U$F#vٳLV6,]h-~YG;qE_&;8iE.H"?3TQ h4J5ܖТۉvNn;TȄ!ϝfM-+bgDU8,ꥧ΀gXp Ӏ^f6O\(3]j#ۮ 'Vt10$eDv3g в\9mEZz:N`K8;Y=ʮ\ԅsS'qM-[ƞk [T%CgO:{ױlQk_0r[ f4Əb;kkQ6Y~iCZ?™֪O ް0XliœџAh4Գ.yӆٶ͛`sr8B{ -U>GqsT7ۀ}۵e dmTHI2m^.*8{G߷.mAOqcvݝWؤ-~d???jUs`y6H`ك%Hr@IDAT5iu g;Dl/ZVnb՚}+lwSNz5dSl51v{/5.̧>}Si[jҾu{ _=V?h00_[v&{v#"k{}Go{1j|uvIWG)K33."P2]\%$mRh1 r=gr|%VFɒ#7,VGrs[/TL*VY*K/O4}; Fͺ< ÚFfic܆ tL35`śXBU6R[pg{eæ*~wCav)LTvط򼔋(3m((*BnƉⴊl#u#?w땵J956ރ~|x>۝7̶ 6wI_y߷e6 F"|?KП,9YOQQ7\4i%h43jtP.FnC5qn|gxz9VjpQGب 22}~Ozڐ"\*莙82m7s6c*[~4kcqh?R& eu4ی27aF྅f3FΞǑ,*vrco wwZJy/ԫ79sl8zi?2F25{n.[al+v)lgz; H̰/#G( {tiG9/|H0ᰉY-\m]A“4o^JY S?gY#H+lU WV>iЭ߹Y6ssβ&7$ 7me̳~*Hy]Ėz*lg}o?[vڹg̱s/>raֵw;}*}v5h%,)y &PA|NCid]uҫ֟U{;3mgWqIxOG>,WB9_ ־~t-x/_ªzn\cB"+caT񓧝C:V=xdϭh;nl5RاzT}A'?E~3 YձT\ d|i5kV=,9rR2@mٰ~R.belA^͚Zvs.nk;ГyCN[@m;9au۞ãJL_n֑M#8?w:trnRLق~@Uɬmtn-I);H~XEiE1r4I(gRdΖ70 DV^ye[Z-D|c[X Gg)t.{nJ^dma//eGFۿe{q#k7`v *'ۡ\̍X<z6 >R-ݜ^gx̤PڔnH5VRrǩ [v4Y,`ӓG}C֠mkh;$u F66l݁p?}l z}>5_sB_B 0rD{j>"Fmmhe[pF .n 'Toj= bdϢI6.$O2A+wKBnAPAɝTs wJ9O$sH/&9o 7CJ#2 w)]Ķӫ2缤 @ }j%A?ͬpҤ"ƕy#l? z}ֳ쩟 y6bWΨ'wñ=?YO+Nnۣ똩I[u`XE絗0XWcj :]֎qfɇK AXDCh۵z߶q6 ʣP'&J+ebC>gh~wf}o׃㒚H|@WJQ=+6G1:aOm̼1'%+zĈ;·zLN#H3&8UNv2󁢣=CY7yƸ ޫ3I;^mhCJCس^lL>!쳮'=d )s` fyOm?ad)b>+cpk_rbUu _ v/9zq>fƕ/}&kW?Ť?`[vH`˶ 2zݱ?"nݟܧ΢n2j]TFDɰj7* _D'Y3U<1 knf˝sw֬XnZ|fI{75e3/sjtM `~UKVuEqp6+b{4m{TzOg>"{ηu/< Q8g^ [w)iδJ] Cސɖa*&s% uARWڤpu(P5vh>Շçu+l;.;^|{Mh#p=@ء-*l8lZv1o|6ah!rar0@M[{O/umY26A O[zxThQ 1; *mRw7?4=$#HJ%eCsa يͿ: {z9 [`(?B^x.ߢaq>x @?;(EAZ2T93}7\m9$z0Iz~:y{ CcL{U.=H^FmjzX[i". 3nocyĖ Goeg>@+@CVLmηB7M NL\ldC-2<8<{ x!LIU#t%l0='SJOa_bo^ nwrrr 1ۏTuIby/;\FeɌʨ:>؁>KAˆ0|a_~w|4l)a'7ʶ{վKڔ5@3h+fwY7 ha3'l}V}v4m(# cfSz%Yp}ѫ*Pu[F.場pEFA^MBVwW"]%Yy/h?JF%Ovx-4߯Rt#ҵadռ-zt5[նR*o^.Y> 'A(Q-\@M58ZѪ#S#~ 경[Rt+K.N#dl96iC%<B<NklOP{>R-|e# KHe>mA_Bh\l ,,j6Q̱l=7!+[7ojٶ ڞrg^"1'E:[mTeF9ܩړ/|;03N?n0Vvn*ڟ?-- O MRjU&q9|IFȯNLUzsg]yv,u㊗X{Fr4_QÜ@oUu8+nmw}#yF?? BhR$`w<"lՇ#V6>v(}ø:1p ^u t.G )hwXkDj w6)ye&A8άNii Fvn ^t iʪj;-Vw?ivOkmz+I luP:.EG;̟N@=ەN|6BQ§ ½ydމ7t\+;ySh8P**V֯]o+0f]R;6] d~"&EBo=pג4l#;<˖^[ĥ#ʫ$ZGp>!z2lǑ%S\nG8st9[0ȷn 6β=~l\s-1y>(2 *wG,+*)mN&e|W$'=&P}]sX(\⻝+IAaW\I39ˁq>Q\ѭPуSL_0QQ2竐G(=`W9L!`{Js<*!Ab44ūTBUZud4n<4Z$ֲ|Z(WͧaPُlRk^0 U'| wʓ;\0ǯ m/hI&,5!`ym]343RL+"RCi'[dd 'q,O&+͆3 5w`G#ˠ3^4ep N4J`rJ#JfNezуGEK?wn<LarG48q,''+},yeN7H<dʓGZ֗|H=KE(z Jnrf %iuܼ6vQZJ)L.^%.m5s>&#q]^_vd[KxM*Y 'e7!D*{!bOYQ.v*eu49sohsGzB8;-RQ8= HH`t X}db ZR< L)6|nO##2N^V=|$ \v9zpsv 8u4*9f0*ߴN^y?d ,:W 9K%pEz'\6"q֫ Y(Ex6 qɳT0Q?-PWHHB#~e% r-2:S[V)dLHGHe\CPlIQo 7{wK'B20^~>MR /"LȪS4uJks@zG ,z/J߃^eՋ<2-M7f7ŷQ.OYE;q KV[4 -p)>ɝJy/t߷đ׉D\>rG33YI`3d6N@VN;*)tN{d*I tu<0uG_wƑo =eI7!bN$"{v;EA.gzt20}1xH0-n)t(?aiKx 6яdJZJF"M0=dRG0l*}3?CKGhf z( =Y8"&ÅG~yi xE/W:%&籀3J0lsW^^+px,닯̱"(|V&MiT|9KIg/Wĕq&-te 8F@/|ĩI(d4\%+.9Y++3ǥ|%qFE9 zV0ǐ8L)?*T +g).YaKR\9zN+`%\/LN/ JaD)tqZ+DDs\F1)0庄Š0}KpC@x=$9D'HvJzJ`q))a^@E# zη#/R\%E\.C7IrZ'0+ 2/a<>%)e;" S WѱpEO7::r:a^gzAaʪKw{&@A,*+rkLB>Һ%"Cr?KJ9| Ȥ W !@۰ ȯKA~չi .6Nq [&7-s6'9=9uvx},#*(Wp}SV>/?nA# 6:nz-M"=NO}Pp _y$ Ls LRnu [np熸H#һ)+`WtOė.4Ԩve8 o GEzEOP&%0aEWiDGAx+ᑜ븀Rr{j7GOr Z |_OYM/O~rxr:|pL')E/)gq)q^}s΢zwmK]{{Ӳr z*L 4e#B}M/z r|$蕓i.oOCWQ}\"r#qybQpb@LLNHwvĻ{S{8+!c1+@. \؞)L~Sz9Y'.+!)&vʹ.<4f uH-p+/ReH33s d(^384Rq pЛl |dg2,\£q@>L8 1`I݁b_\ãJ 4k TB s'{('iqW?=ef*[H(h2#$%ȣ+(!uk%?xwW.z`_ܡy?N>jD.ޣ!˜`W!'J{$SJaz Wfeɨx_qs^N?f9Ҝ0=H _Ǖ6o;$p}ܼҜ iO^HzKL0xxs])>Ryx):\TrܚlCiȇ(3tu o8آ[[|AZU8ZK{Hz r4:GRA玹3:l~Hm"'>0BFA⴨,I3JuZi Se'XYD!sIzeWVL;f `^ninK[O?˾ʠ[1e?^X7G|jk!U~F&eEQQg ¯k|K9⯙[pQ+"~„O{(^ 4 ,\\)fgACWf&/y?1!eIʼKI2k({Y8, īx6C2Uao<@;Kk͞e`"E~LFo6z 7BIҠC=64q.€d2wfHwوlpCIn⒀GOFOlG)~6r69áGMB.#+6E+nR dB`6]BaNe(Yt 4%O)hA8GeA] >XۓxT]i@~QH[vr ~vY#qC(Se? \av.O⩴.(6z偩.ۨ !yПw]v%Yи5uGmrE`DeIu721OmJuAuA熤kЭsCEf5ڞM5T772Cy w=w(VUVj31i/T?Ty*~zUuYv~xJA25-)#;ձ&-ܹ7Ý`d)aR"|E_tMﴯӤkm.#{~8x)6RHaQ2t4.B!̤J>gwٝj7֍۶J3x+OW08z,|N`Dis5t0k:2fs5ܫUU0EV3v鬲hvC"f}oFI}N{VPxOV_RaGBS}txT7Vq@.N}n-`&A tgX=~:3yx&VzryjΌGux\J90똥I;m 5VJŷ,e1Y7qI;V>1;*푗*q6>tm]y2e·/L?K>)S{I |]۫ohhGxh)N}tןJ ,|x0?N0Wqc+IgBQ\*9qa>y/[ \K#D@㤗"nJHO[?=^{;d w!r\?"]㢧D&'ӓ%~zSzť AoA`4Y?rp~r(ӛ;8gi92d.$^SB#w\"UB "9ʁ_@IDAT*8/l_#8%UGZ̟!^7;Ex+(gwOX]HM02R yZݞ)w)ҺqOiF l (I@7.Ù{g2y Egʦ1*2zr.TAvůWVJ{zؚ*{ 8N[o^/h"Eet@0ȝYchBsTj둦wT («3lQY" UϞ.*0܀b;s:l*10 6cPoDYEj{9c<0N'ﴡj[nr>ѫ+z䃗۞ l@Tio?vY3 O_Vi3O/HrX=d4/봻/m+kjC" ن5Xӛ@A7وVۏQmn:mͮ=> qJNdVݜj[e~QM`eyY_QN?}갏a3QuK7u ޗ,{2←nyvU~4[]RF '+!n<qӶ) $[Te}촻OfU̼w[z#߽m L~󾗪ts+I{F+#*'I:m.xcp9h٣T٘v)J |7Ee O4`k /M]y'wG(WfNa-".ey=< .酋"=F-KE8y?ILW䝐V0Eޏ&+O^L.OQ^'S׉8ƻ*)/.‚v>?KV*U7-+_AV^Ix:YP=-gOy8+Q*A2*cɸ3b@O[yKe'9{yʁ Kqr'zc,"U+T'4]?_4Ӿiw&=ϛWUw?@ cQ2mO@H9>'1 Aͮ& :)݋tEG1Qܠ$0G^!̈C_3/OZ797gmZi0X Vm5 f+n _^Ba)<Fq ʿhc0maVv6)^evښ?$'KcVMHfTif_Ur7"{OUrnD1`*v3Ҁ? ~YӉ'̀eWN+:mԬv}q>ǩ(_3 N[rDsP@#J2ڪ|'QH~{?PiVfEBQQ0UHb}ˮQ4{ԁ9g3bg30* F[ϼ\i Yh˟UQZK+Eq4jXހGj S8Z;z:'*EU6wBuUڃOWY}H - {oowÀasG}X\? oȆ /dv.=W ˘JSV^|^e3bul\o) p*{Q|(& f!M0H6Mdx.*oP&# V,gu`o~D4.xv_j{֍U0NL|o-38;E<_X|crUSq'7rE mEGy*Ҹ_ #n+ aiFzi[Ax+]0}x sBL1%COǏI\ ;Bא> lF" ȬY0iw^i(M|,g0f+[4L궔ucPy^^[WYbABnۻk R &;TGzfdOJ(F TOxWޔGAN/ iI~luJ+Fc84"/3jE['Yi\̪'Q'S! Pc`+Oj`>'|<0_,K/{J!ιB܈`H2TX- FޔF]HWW"90#g1Nt0V^gW$bU[UtIf&>zxOaQe%?[~Ȗ?!09[VV§Ę^N/Y{?n磄^ub/m@J?p FJqD?p" L+nޏIOIJyw\E}ы<)>܁&Wwzyr"\ <%2n &%+%\E9ދp%\e'aJG!".z8aN:~w2epMVAC<(i_D7/ I*ŽE/⋸'#g"+gyY^/Π.|bL"JUyj86`4+,#xN6dQ(QDN^)?h|2A>"-C >d(*Q×n%((f/ 7IX/{FFdw,Gh|+PB x ny7ֱۙ {'e%v"0 IYaVKT 6WT< ۴EGn1CTX=Y%/xmI (1p: .vW εHyZed_.vxvPmO^t94,@{ ѹUyno )o9F uMOD d&nZS1a %Gyai g{|V+c,T=濷+An:Ip %:$}zSI=T)tC&SoԕCdZWj VW-ҟ2- BE\?DExWY,FF[h8!Vs3Q:˄-wŠĸ;s濞.0D8Yq.ǒ9&8 aD p+AN9/KzzC?rʂ=Aąz"mOrJ)A;^Kzx)|!ˣNO΋r0U4G1eD p9/e`CVDqGH29|".E^'^.& p?J [Id \*/a+IHe@Q Ltc*b{7B//2(4G0g.+Ӌ,8WrWLJXIQ99@$rwE:=_]".lEaY%q9c.0#w] &沊PI\ h7M'!p! J񼋃ʵ oag.h+[tCРIep׶ۧ8Sp VQ0j6[ק/&u܁ Q8)[겏6Uj{%^ꊱ皹R!<*MV3@[9­S\$𙷵ۧRȇ_K|42O"D]_@ Leq.,>Tl [Bj+m_uy)ckuVvP/cP:QL@dfey/~α|*/ޚ}25HU9ȁg2\Ri=YaOS]od8)]')۽2{ji fu]mlO‚); ]ePk QrǹM U\$y >9œ 0'@@'^K`"\0s])+Y zV8v\9=P8:~<J'p@0*] !\ . QI?9Ax=˃S\^w= &xoǢ 4}+{G)VH29,dnQJ/{JstO0s#zrOXJqeR޻ $yUd ^tpQzKrx8ŝ t%q%\b?yH+[aO2C._$^s4T g:>E}~yNv9ѹ,ytY6ҲUSjzUN3͑ =O}\#-FΐU~. H:. .w?N[ ~4< %|I~y)e֥;{?gF"(Hz;[V2F!^ ̼_0˶<J|Lf7݋"vJ.gǺ4ѽ硄gVŷYJM LۻY]iǣڤy.$Sy?u= (Iz)ΐfȄKntמ6]dӀHJٵQs [2ydVȇθj{lOm`|dvZ$~ !ϕ(3`?LdE`7ۧ.A{Q$FWB2Ɓd0+Zi 6Q̐/Y1Ź]2^ͨqQfuSS)E3eZO# Y,]U@.sr8~pA-r&ϝo &Lvy0Y|)N<,T_@XΰZJީ* Pk) z,}i9Һ :ڮ.cܞT 9@kVe!Kfp|PgFPΠq ]U%^$˖,Kd}Ďb.@ASSUT5HHvwqF-;$Yk묹>{߽OO䝽s͹:g\,oY>췔,"q\Mal\-~n_, ;o1{?ş屇+"KjhLȖkqF)Dx1$(+0&LS8W1ƞ,㬧I;#yHf1u rtM]Q,0?ۏ<<Í~CͫxDm'$xv<†8)Iw$wdxN'|X~~ NBIq=+Of 6rcPU:8y~ 4,^~ xL_9znxFqN.<\dΆ "|o-{8h}Lu7'nrֹ3 \Hp?A"䑉B81C.?w"pf g?KО?Lpk ;ۊhWAq8YF|\\pMVoK: @v#1vCLj.| $₫ >?`֬Ϯ@y`IxuXÏjM/v/4ޔmTߵ8o׺l72O$*^ 9a 9?\p7pgD26 ~.w#C>3L lMűj<8K..e sB?͝].yq; p.g/<o5Cxk۴\~|h/|?[>}߯ l<tP}.7adK??FjC%rܱ0s"Ůգ |C5d%L ӌ+AsR z\5 M]#nێ SR"&P1 Cgk1KW;aЄK<0aO\#P#ߘb$Ptߎ@ێAĕbbHoewLGac?z.c, fzO'37+Xsf[Nqy\ 6|'{ ><0tiap$.4X+qIchUj?іgU5|?GnX"0,Lǹ9jX5A#\k'#v_h/bV%/D.89^h>+aqg "hjwm8+˜%MxƼni<_ُLa.3<aq\r\$0oy|Njxafs.O9Ƅ{>`A?:>ڙ\GqaKn1rP <{i,`|W]σį|}M85MHmvs= 4'ow'.,݊!x c?zr _Vco|a,F)W㥗kT1)o>wysņzNC>˜ v6KyF."iTU.i8>s ۜۄwz\zNIY4`J1OiwxvIQiwdva;^i;OF+Ss py0\/9o{Sy]r36gmWbqgv Ꚃ+W² SΟ|coM9F<8'aj!Ox)Ip0|ܥ\DEy6ue&(?P$|T6U2S!83:.9Q<(ÐRjD!^գ r.!?ߊ)c%l^l Lm0ip}\1 j!KF3&#Mυ7/ŭAXo/9$M40G =,]ry y#޲z~{e 惫\Bĝl pZnj^=px4O![fW[& <, zy̨n=\O+.,v<#5G窙1.̠YvMى 9K 5K\xXWJz! +vqI~.ǸMgzؤ;&5g*7K/ð5.z$zaTn \?\1(+ipں}SG|uSuw\!f70{|5)?2B6r=lC-+$''{(U9mBk4WI4Jik3@ljbPW&bVULxC'"&tRG&0~4ר|ͮH%8]R]ѓ0עEc |_џNc<]sw[U~V %qKcޯrTo+/ ^XzAҾɅA^w,>ya|gKE5%:F-<|va4tW-|>8=yϿ=@myȯ\rGIS5mEjϺ龉s Kz\b1ayNfzrE!+*o9m^Y',WҼTkNTej?uRl9WgIz섦۪z1VdX%Ej7z0h^Uk,[ re\UJ3r%p8xuҿj?̕Z=Uhi@aNqALϼb,BGkZ쿃?o;l-!n9I&vӎHxV#\HH;"ֻ,rPP()\΃d-EpoA1Ǟ2yTc@>NSK4V4dk 鶟J,b/1(aO+Ԡߵ0w&:RNB y K?chUcKcYLl㯙0qr%ťHDar.P $̿]bmKPezy% pɹfј9^jsŝ3 L8Ӗ^M!% /ś Иb1j X8S ǁ|LQB'ǵX !*&/J°?zjL`KDd'΄wPyL&}I^.!mH0X{`h+͝V.W~u?gXҲ|?ą|L&?lo*è1Wմ󹏪'dyߤ)tw@ >j0fBxr2wrigu q'lBO]M:  /#ājLϹZ%_P%tr. %dg1I쮧Xcg,9/,.Ǟ+\}ƣI[\\MC83"IQǫtT"J*p%xtT7`kŞr=s'9ÒٽO JT)՘Rp#ym8}j\G?iVjJ1 .4ܶ-W ŧ*,v:Xn'YoAw{ !o\,|۟AOuȗ帊hzi\_q{6_^l0q)d#cg#Ҹױ}CW$:3ai ~IU1c 1(Qd\Ql^sfz=}s[-gc<ƫ'Alr?N  vR8 )IL4+z$[G~zS߅brEnZ-Z۬2QfФW66ڑ)N8pr0\9N%S|S=Q)gCEhk{U/ʋiqGL4˕ Tpj'/M'8k鱝h>a۴odsNv#$FZ=!Z=3C^58 1M0Ts'zE-[mc1)Rh2$1 އ7 m6zx&0E~?,F5<[Bc?Tt~°FK;XqOZG:= լ0&9U?aD]2` 1d~9U~';v,O^64?~34&ywLzIh$.4J*Y\ G:*~<6.&whϱܨ~>F{ߡqr\Or۸*c2*l[>T9R9d;z䯖Nܗ*_+eّM!r#?-#tm̯U@Y=& =e}֖邋>uI1M'z:l4禱gCpO}xwۀ?mŊ|&_kBhw'|ZSE YExyKm 6|אm G*\>1㜶nƏ?l}ȶ g*"C՜E/5`-[>FrdkjNQ4C:%\842%vJ)v 9\ԡi6^~%\i+НEAm% c:yQLSz*RLs #pX#c[OSW|+\e1[:^1_|P7;Г½!5WUm[17oZ"?\q;js?z}LvS1qJ Ҿ\l.H%0RN2\UQU͔N`bɉ渎(QfƀFIS;caPsny6-;ungx}  {oD6a`pb'!Rs`r#yG<~wb+ Ya? W^zVzEp{79?ژ6N@. ޕCvi][; 3WzA1-DLBsf[SWڙUr~ŹX:r7q3J;T:2GıIQV100S:wtPW*ĹHO {\ q*M؏{ 7"g1wv9IB;NI#b\z)os2ZSJbO|7TE;r~h 훆<08/D1?t2cة(=Ƃv)k }+j7Xh)\D,U=j;uGPPCQҫƞJULC\.06+εn~1ҾyP|j0u\y/IF؎R,r%=/">OڥcSL;ÿS|4H6jn za;.7u+㤙"fC-[m>;^m w~+NW˞> L.mnܵ1ip}mW=wأ A,\V rV-8_xΚ=d;wZ N})f+qk~ X`4 )'3x[װ)وq0mԌ v5`D@0$<]vYQ/}A0L{,*vwS뇭o ޽E/۫dɓ8SI\nA; ^x{⠙2}O"\TWn{ G}jٰ?n?g&aqk:1HyhuxfZ gBXDH:TP߫z͕;Ǧ^t&qsU*\zѼWdHڅQ2.ҰrNƕ [Mz4mr\j;W`Q,E/χD~Id7=J2ʥlWp^B4?qͿCIdAiW@ϪW -S=ih+QW)Wq- O7q㚷>V$R /)8~˶dbmy8I1y؞]3k,@ y~ ݶW~![zݎ_Œ~,2_4ᄫohY8_ r}}.Рݸrz>.Bg0XD_eӮ Y5e㖧sg>lo~ 1{l5MvÅ7pU$,VdnC X,w N, At&' 4L#1<pcOk["ĺMB }B;Ly=x;Ey{4.p%\U}i'GOs b;(.ihzOst 'x?ӮtsUMcAi'u!E3yO}y;z9WUգ6Œh;O bSkPML1!Nobv`N\Y;jb%=SɱHi1?{NĜLOsbJ ^ A16°v{\qIڬYW҈P5k zK2.jc0M9:sR)Gurġ0Ʒ|#r1PyO(r5069IB 4v򅱎M1NT8}%W)hs5ʾ馸& /?D{.ny0Lߴ|ؖ'~+皽2ennKڌ?{g=rĞbpRnq՝X$mVM/ų1nÅ׺wۙm.bߡ.;ϋۇm9-W=Y\Aˇ٭ 2؇c|oB [oJBqS yI#lU\1"5z4Gqz1=ƚ]'(ҬQةqߺa@IDATN\l;7E.a`&cGIzsq=G [sRY>^Gm a&y8.1J1[Ђd趛pӆm!۾+𕮸n[?h$n,*vq5ZqO:?[ ,R"]È <$-ZGl$\xq|nJ݋+?Ȑ_PpU%."fqq_Ѹ%=OTE6ʼnڹ=Gӓc`4\RĠ$hQo7U)yl ,0yvmO7lu\!?eBu=C4hLc)?)?#WA5Ça5^!nUDZsȏWXrE r agI%? :ѸߠGBrJz.cԊ+nj}Ӂ^+ͥҜrͱ;P;, <sE::vN{;ziNߐ+$Χ0>p.(9fso W㘁HGuk;btm%W-=sj7m7'*W sE/C=;.F颹6;GLS%t,1HG5J\Ġ},v9ǷϮxܳq /{ o|y}^ LB;pb n䝂8F|,id0j,|"+$|cՔ)H*e8T,dlceSpU7n =Ux{A{ /a3F>>>ɣ6qaqscEN*ځOFLZA`va.`_N".tsEZ c*vƎAip%[ \4 iv i%=%ڜ e`AK<-%W!=,R\a̹b.Εx_Q{4[ bH ;yXWtq*%B߰V11Lj<4PzA,\{Sl9nثzcnL<ؽ_  Pr*D"ݦԥ<S~W;1hkjg0X.Q\n7-Wfu|J;NJ%h8*8EsMMO|IK@xjy r1A vZN;m2nVe89Ί'͹Hp$=5WYl^GU)nh||StRԏJ\D҆x֥gL=+8)]/uzxyx'$~a;㍲^` A{UZh}6 K![ۭV?Wk6g}x{xX5d/mϼЅ+~qn{z.{oCXF\wqYĈ; dkQ XXq;xƕ3)o,L{۠;<uLJ]̂ .JL L^䒠` ƁE#JEI[µGR <5\<@2 q`%w$"= ha8ζtUD\zS^lj .9:=/^T@㚫,vst4-60$W_hG>KIL46ھ!c"p"w!Z\(9i5q TᢔUW'^_`+mcEzظhN\wl!\P)v7#ztuN8_.۹&M$.Dz}]} Ţdx[WD>[fa.n a1bzMG1\Րl|( O]ELu\MbX9$ұ:v7#rbGpEHELIJzMb/(ұ CT2˩ąE޼O$2,\I/uB81,£ٍņqp ؛ DL;әVLɀ'W]XJ9IbX+Vljad=RSjXs㖫PIg1ec<86bޠGG 91DP2w{:uA`͎/\IU=ڢtp&gQ9̵gg"bD<++*u;}󠃫Wcm)&gqĜ\T`"$G\l8WS* mqU1IB@j'?j/sFSȒţbH4Xp?Gǜ$|Ghpэcѣ#a$a8Q|?`״ĭF˖bZݳ 6r߱]Lq &'"$oO♊6d? ]Tl<99)CO: q&ar۱e\~u~.^]0v\ Cpƥ%4vڹ4f%ar qGlu\CErpgч>=\|\xĦ[§ {r|T=͏>o`S| Υ|؇rGݱ$Ln/<B޽W(nq18qUĘۚpBƞ1N؛O)V= bϻƬ$Ln1( qAc·N!g bz( 4*z9\o[c /+b|Yo[/r/TM Z oc8W3 lE 85F@<=X1 IN;BaEj1¹ib7Ax0DFNGq1ؓ?4Ky^j-W1/;gρonnWhSbn Uq1*h9W,$?q5=qՐ73^:ܐ7Lc1C]g\(5eK%{ ;,s\[`H\E{Gӗ子+q슛sұ|~i5P +Y\͏!sLPGq0.9%)9WQ9OzL.MjbOuL]+b˘Eq\',ᢃIJckaR >r@<&s'(屒,Sc:$aC'3g zk]UK9QtWƸoGjcXivoMc3B+O1tsG(1WyL{z*Or{zy؏Un |9$$19.UL27 fs~]Sx,v(wI#D`$š!9WQ 400|J-y^485O6:u6xu9"&?'F$ hQS?#܊<¥=Wh=tQ1yeShyp8G6ǜ;!%3P;W47YFGq>s#[cw t.'+4&9c_F[۸sTW͕a]҃5r=\8[Vz>VGpNi1K(G[+yQyfq(uqMp)q9Y:Uf/=N **u1/t1&ð'_Evҏ)Wҏ:.W%o{uB_`63U Fa?&rU;G5v6V`S;aCcx8Vz>֡SE\yLxh؜;aݜ%R4jEi VQJ;49V/aD(b {U/{9;SݍAN,7 ;+YG1,>MOK"後 m''y-=xS!Xr 7ġfksE@D3Qc? 7x01׫rU>J*$؃EU.s9FG(+~x%BMكO2 F!;lΑc3jV'k s̽]'"|vrF?ד]\{02so:U.HqJ ̏S!>1'nMb\)%U1E'԰o0E܄tU|j$Wa(jʕ%%=dqQƥF1}KG(ҰU=ļ+b\p bB" Z+㢟b|agc~X#(-a^+`\[U]6&gb5qA.c2;U'⨛J1b`@OaD)*WTM=:8s.wFp$l8g/1(+:  7р1@q_ƍFq k[/`8]٨TsHOIة q8vV=aX0tNzcXF=䠢\sKRƨa$=#, <p΅%=i#zawEfzƹiszHMK/[<)QrE^K㴻.pC(sSj/Fc (W9NQC,}).TGaRu7RⰸTC19FczڨWH耗cʕbVQ{ІV*"&G.'1T5z7jTp-EPⶈ=aX0U;y+jpC~BMn(N 704h~O5<.bhtC;q/=zbsE4#6ZE<$,.*vb6QZ齙Xg]]1Oq>b^9V>1تS~˹Юrq/8AbјuwĞ9< 8HgI[LtIȇFbGI?0:v4=2 zP0jSu܁vFp)WqIt\CxU98@bW1&Ko<9C5NI>'= CǢ:k'vٮS@06+PeP#.4ڽo%j'>쾗?nlҬ)SDckn83_<_8bVc=1!ښ]8o 灾aG)L X~I"~.ZO4Oɺ^w7fLaS'hI[)9U,B^\!;8pԎƤxq|ۆc߇M;یnykt l>xی)۷?l .԰WS\yhN1nGcyy0Fgjvc7953զC>qUzZ>?T6k}-2r9شGxޕ~SBʷM1 ǬWr2))nOTN瘕\aR{,tt,>T\;Q>ꉤSdxqURW̍ ɕtTۨ>2S':W4v>ѹj$f9rUD"=Zķ]v 0ߖ /ZgݓpB>J NBLSLC;9?قW<9‰'Ww>`+L{Ħ#Vj<:8Na u͛=犓BqqǫGP⌁'ćZ06wumNa{#ƜoIiv5ڼWl!9ΪmvSs9HqlFGb2ɮJ[r\8^xv=~MlN=ϙb[oi;5sf8={[vيvtv{d6$?ͫzcb'0^"?4~1S Wh'< 7^¿؁W)WD1h86T oZi3+w#RRsLu\zts^+k$ ɍg\AңF8$_f3*&#bNJ{z'eÿ7i'ոUn'ic8 .o{m٢wݾ;~6}Txߡ6϶&IeC5FvCdčoLSgZ?N&y ОX!Fۋ=6-ܦ܏,P*+cp6=cv}uNŠ 7qk[oͧOaD.>Xr lCj04m}}7a2_[mfZs}M6wf.<#) G7^>`}CXt*W.M; ۍ7ڹXyyrM>umG-]a?vU6^۾k-\vXW׎ H>Q\M"n]v75,[6vڢc_y55̴_[lΤ۲c{s^韁8}Qd`޸އ+#u1 1nl6pG־`/6bTLHWqskWWcyh);ҋwh}۲^9!שvZlyBݍEږې {.8s z̞Y}[vδ_"oZ9syy*D ^Jt:rEgۏ֯y>ٿB{/|.zە+_5mw}f?m/>gO?[|{/מbgO+h>sNY Ÿ1>f[ i}vΚj?K ,W-Ͱ=S/[KdyHbq3j;;9oI3YJ\W`:.j~HMݩ7sh+^!U.ވxٮΏwhE9^zaoQs4^ג 9߀a|A;CaAQD_wgRÞۧ.=$PB6Wh3&xcУgq\Uڍfa@lӦ [CڜSN˱p hoD[\[8lc1mmvƩ'&筲% Ek;'~}t ;ruYgGMc?wةsoJ;~k߲g;U0unIq7Yu{v+Z0e}vayݿ_LDa3 s=)'}+.9s=|ם䍿N%K/.6+ѮLb(_l[d}[Ar[k矱1nU+{{;띷؊0n5/]nϵ3' M[8  03ޖ3<;96paa8w} _ S0>w3'Ivg#cyn+^)srT]=lx.U=nЃy Oij+-sd+34{Sqq۾S ämvK/-q5 *\9BJ;7%+ۄ\XU*Ɂ_YyiY슻u=f=NքnUq:x2G=j(qU͕$v fz9&<7t DO:V=ؕx=W Ҭ2ɕm]Lsw?\i5WWTN%츒ɸģ78Eo8eyc}/Xs]bO=CټyqKqoޝow|[~go;K쬇oϽg̻m)6`6{la9;`}V[ 4mRUw\v>8 1/?~[,'rOUu`ٶt {ٵxwOg_vَh΋Oo㛶.?bT81nzyzͲgY;4TvMg÷Տ?lw?fbȅhJl$Rq͘Xgr~MR&eTu%1)t6lܞY\sGy\.Xh\o+k#;lso"8͉j'ju:=\\1'cΕ}1Qs1媢n" '۸AQ*An]bEmÔ0x9F'Ǹ@6KڴISzAVz=\yRӾ0W~l0h6)'(W z EmNqJ8iD|gXrw %A}v8?w9A/~cOO~m.n{'9O|f/cz7={p#n YN?y߻w}𣟰o9o癵OP%߿{}Kh}'âdneYW*Kmil`,.瞌7;XvY i6i`u|=`}xv-=~2{_3/_fO-IKf alH(>/K_nÇvؗ/666G[.`1~k \_v?q/˞`8WOs2}\v40X4<#<%䒽986Yc-_aO|6K]>-9ñ h?℩]CUGW9.k'E8_7?A͋b"U'0kIzą?|L+E){ĉ 1lǜX-yXrg88y}Pvisle{[gx6y:N-9na3ϓp|,4e;gL ϲg b7|.-YԶ##E'|3 'qo"N.Vg4XX̲Ao{{'D(n(cfG%g1lUflwMJ{ග8 ,%i`͙תygզh7c<{ 4>w0mHuLA7r;/逎t8OyzǑbo8qm_}]³m1v<.R{{綤ŭ$NM9~®Cٻ?9{fpCֽ'܅it.$"`Gq$q~S;g@i ;o{qkwm,n; ~us?dg烰wm3(}Y|ٓ;ıϛWn;^9`궛0 e {y4}x}aj*xdfw=o3'|,niO\i=}w۝~ }܀N'{TEd'a~q}f/`0TtWq[ .ۖVUW\QLWE[NyKί^'}'/3fcx''u,Q +wXۉݕ fj6?z~a<+%ߧvr\o}܏+顦 9VZ|A4yPNX(?ņ|?7=$ :Yz{Uڦ'U)V5WO KU1%7ڡ,'?nX+w. =+۟ ~'쌅3p} ~N^+ۿb+ 8I곩qn no+s.ë0nxsP/xi+8Ym,^'^,vnl)Xl+ ~e7Y!<0M@}+L|-|tۼog^;ppYŒYg>Ka}|7{gg6ӾzǠ6Vt)NX/\qȞ|q&WIh,W.e_J1^>\OO(≸NƕⓌA YS}_p~ڸ6e!ƔOZE;04^}<i[<(ֻo4{m[6˦r5u711oR)}d1x9m/ձɶm8wԳw>ZNnF2Ӿ3XgL I7=q7 <Ү_ŕ#v߫xhHΞgݷ %6-1cWߴ6'v,mb| ͶNa߶Boc4yٹkbwg4J&pLH5s.&}`5&z<.' dؤGw߹ş9 D'9 o4? 6N&s `+I'hL^)WYuyPTaҼ02.hz.V+J֭ `r)Q^~y/)W!uǢWU U7ؕk2cUcWSqEh:9&UW塯sq.-aK:V7Uccz ¨'U8_?Y0 f:b>#qKlՕ}vi <=,5<n;wC/kOSϯn꼳N략/~!{uv%? {Wpog.;[;_ۈxp[IO}̞}D,c/>z-/ڊӮ_O}z~Yr W6z`jh arϰzdݷȽ7.#{AQ \'cE1Ĺ]^s,FispBrՆ[}%tuW^|o*7n!K⳰7*?IiyKON''vɖ/~R_}q ͽrNvzN]xTjyv.[$؏N b\<5Z#>q}H5rkĞ8_%7gը^yj'ÛX oo[63+ڋ'W/ѓ˛d[$N©keEhwM+n/;z|Hvc^6!dp87l`>k}AA,g ):&ǢMYksM>&H'osΧMYeݵӏd@͚6Q(wq9GjXhB!SR`xўG~gY\6㢱- bE`f/{P/ Բɣ]ЉbX u2aqٌJlC@Ah6ViuhA.baLj\  Ď?r7Ѧ PiOMZMG7s,7>W_5R>_p,7~ec)d!gj"M,Iw.tª<jK얟H6 .]GО2]8,k=1ٺaܵW7^z Ӌ/<k垅0I<:$mU58#oom9cdx [{/4mޓ/J*u+g"q;Šʽgx9s8whyxl'eX|U8I}wmQt;/]Mrw)&W1,pD,f}.UBݻ[ ߶Qo>N?ze\^wLtHm\Ļx|§_qL '5L,'O+IΝL};r 'WYÇm1ͲUjb ׋>^4>Yզ^QesprqZ rwjO,[wb4#=CpU=<Ճ$(#N&4' cłz>Gv,ĉą؍N֜p2ilr|N%5]3~q U.,·3/><k׬W}9 oO!uK{ w"?]w<+۞&/`T}>xr5OFedGW5慛s8d;`!cK2C;e;5c>L,,>mmec)c$:vtiTB,!ifem64S!My|0Vؤ^6w4CmF+Ud(eiwٱIcUb[͍fVfl(5V ZB2MA7;~et+Ǿ~EC$ 13%Ħv3rs^./[)S;]:^Lzwtj8i³xYy]{ 8Mᠪ^p+F85:d_]:/x㝉c≄'b% ߡӈVYǽ.v˩ØEG{܅Уc:me`ᱡfĚa*3*9<} '|P?1xdg :Ā џ+bA>-j:컭8"@;OFqOCSǟLjhs9x5'}Nb_}Nٮf Se?.Gf0c8ܨtCY;mQUYW qB?2lb SUj?vMsev*^TcrI-`̙/ux|ڀvu9 evjr5N6lfJaYcG!._ܸcQEX4.q4@|ɱ2L757lp㣩lSdPPQD}F\L{u{u+ ]vdK(0sCQAWp^aW/yr؀IݡB7 s'[bc؜]C(l}~wXH⮡2ƧQ>ҧ^ױ *ƔTC>R3Btc-^FIYyХ/÷wWx| w!hi?28V|_O=nyBUqŜL5B͗{_~̎ߎqЉq 5y ?q,WdE#t򀏣q2G9qe!xg'*x:؟1A1^8CUu ]IY̙T"r3j Gh4^uFACfc8#xt/t{ϹfM2cLq|QAs86rJm'"Iz$ 0x)8yËш@c |x1PxI xJ=<A$۴|Oc'QKzjdb c wbi"$9/aLm~/{s&dS9Xmbv|}V޸5-RbUb|YAli9Xf"}6ݴ6jb&j qvF{(G{96㚕X] _x2qLדG|lg5nڹ1b|>Lf1'Vg3VV!q|rNQR*U+8鰲i+:Tq Ilg2W~|s-t䘩$]/Y{Xʱ(x ` [M:"ms<0D .hqp W'A~fvLӞ@_Qc m5eIA798F|9I1N@O1M}&:9Ws$qH4}&O;grP4s\ F99;"nEP!9'h,V~Ւl$UT{7Svv$mZF$&#kёv-?ِfk׮ sMe6+R^6f@mvDIˎRcǭfʮ.^ʞC侗&Ig>3⃛bS! LR#aE|) 6;k|U<++ +8>cL)b*V 7;8z h#RH4[ P?֡}OaTʦ8)nT.7XIgnjvߑ6E|Z/=t1yϳ`Î&uS/vu>PTJi_8T Ky SM"ETnvUMBsC-!4D 4x|yD⻈61V*4Z^.x68^oX߻rc<> QLoOZ `!z#G 9猕z[O"R|!nk6vBL)Wvf/{ece!{/bPyŝPؠ~4>^OyWzї0IthF;lh3 >`)X#jnifq˵{b9]=>V>4rX9$b,>XG1ˎU16dר9ٞc]k9ҌaPMX=f؄vm<4smgvn L{tڭ bh6biiLbEcX̙\{  `v5;fY>^5(5W<>3/g}]M*14#U>q [W Ks "VrcEMCOXP<89`q|X9tRxnNgW4Vxt1+15X V]񅹡 &%&R "57`sHfFlub'hgژ-)ASX9)?'-O1eDmDACvJ &jV!1o%NJ!؈, A;޳XܸvP%:R<>vvcRbq]XxTWl vp,W6b7D2tZOȴ vy6`nXp, :w=^KuX#BE;b~%5D_6 A{2ӆ wާncyX %Yq%``b1>7v]䏱X8ahL4p9&r,%Pj1~~jq{l]I+Hkkq,g}ٯ" 5ѶhtLjn,)nЀ\0k?QLrumc 74u vY}\&AM>pyl+s]|OVlD| σb 2 ڸ' }>h'cїX̑R|@_ۙ8Šm:tU+|RX>u94 K}C A;A#X{xl/3d&7F]Mqd%@V18RFXsn:c]Iۂ |ў++'h2Lc l㞊; 1<,|?a%lIb|Y]5b's2!& i"GRb]yrs1l?!KxOkVO]*ʱJm\|tqlIs2XslXD{OcEv6$4v&7lb>xJ>Sk5/`*v;b lIqF}Xjq H^-M,~\ײжvۼ7>KsOĊJLߦ9ב 8bX|T SW_ү}a`|A/i/a%d~)_HK:G[&MV;>~= h XWu'Ҙa;%JU-)/>5ݠ /5r7T%1b_cǑS8SLlcM^旪ذyq@Xs$c!m\ˉuߵr|%m nc:subhE|`A ^د`<,¿±*um^1lh`**:+zlnC/{_4V?#>Qx]މl[N mc!c5}c 'T(=r>Z@ԕwlq;-c$AX3|u|>fa]#|w_ozp*VY,VޯGxNu+:Hܾe8D]|q 3D-.l֢`XtՔe]--#})+;Ě88^%G&WUm~F|+T C)PgθdeXoܱy\8SX&A\KC\$-X8m50%ⷞ}8k;MV[-=ӸŒwA:=b}_'q;8>[eT/ #XX,,ϻr5PTG8Ϣ&тO–Z B8Cp+7KFI>UT'3\޲ZvhmC\9@ jfn{qPt1jEj-NS!%/\!W}lgrYw3\q3d +9ϊj eChRet]2 .'V#w)uZT{iv),'(OŇvL6VҏPN&]\6[\4X7mR;:x՛WumkDڕ*%Wdq]_Fūsp5=qDWkP\p`86tȒK坝oXG8LUg" &z€yH V֑@ @t jxE: (Ö\ LtX,\\n޺E Okqr5B#R5ugj ā W_-k/幽\t ֛nk=Y8߽O:t+s/;(`*ub;dؿwQja{q@w,?ESG_3SҊ*ߑ f\wqk 19h8vpwʆ0.+exKlbdf,匝jcy5m X Fmj㎨٠40P,c%hɀU;b+vBY%A֮vwC ڲ8nnQ+iix.)VS.m`o.ִO60r\,s$ hsRw.W<)7π PMM/+y|h+ws`ǎBXIs(h@8JFІLsh/n:i(Rs$եZ_n|ȍ%}T|.O;Sq mG5 ktl #&/|&a̯l>{2sޭuV2v9- 1XaM|hSwD%& :S7*sljҨ 28tb [ ҂Q\mf\wO/Nኬ+Eў1@ڽ2u+(3-+Xݚ5O &G~p96y'/#7<kqO$:v͓֖Z=шbDpG]{sX5 ܲr؏c%뮃X{ mҀ+ {B6=+l1ЁX؉Bx:䳇/p>KT ҫE/W>8#ag^BqdìH/d{dEXȺA=$Ͻ|\jjqDEj909oU.Z.M絗uz' r)i[Bx\qyr9iCy/ݍ Z 9~tIlz,_=/ [epxJ>W.Fh^<*5 ܂fhaɗ?5O\+p7V*x!W;.GK;VJ3FZ/}>>vZV_}|~^>.g+W$_f;9Gjd]8؆8T;;9>$Ge놥rrvVaGo^F<8iq`l.9on9/pgpt`C0&S,)§;Y!,1EX9,Q{c>vYSs4XXE=v&m.Ek:8&GRcC y+('֮b}擝?&\ݵ̾+Ċޞ*̞\j/GyLVq5tVXEt|zC]a&\  I46Kϻ䩷 +7oM-crp۲秤yq@}υ.Y}FJVN9(?GpR߶T>Jsmtw`jӋ;KKsÜdNZunI4T7OH#HMN.qZ>dYGn`ţ祭'}R3o<`o`c454˽fD{!>S޸P-A.y'QI#5<*-H- NqG#*W#5]}qѥ8)]K;a2H^ǝ.w4g70P-/t4B{?x]^鳲2".>ų}Injy#8M\?Y]iwOï~r |b*yq,YM=x`xֱQA$ <3sj;{oʾy%-Jl߉}sDR?#saQO-wnZ+QZШ'?Meyd9xOtW,orՊer~+/wz%ُQsqSkԴAvaR+k'OC$vhsc97%lc _EPw6u/{_ >5b>` #}h6֧w9|HJJ+ כr}4sLyq\BWLiE-~EʱҰy\jnb\M)sù-Ƨ΄5q`;bYvگ)9^LXoO+$DZT7Y݆~>SqX9}>DzJqMY>|Ki$fܱtx+kdCic64.4Bˍ5ӊ߻RR>b)/VhOiwϐOݟQp3 .8|cr NNxW\'Zku LǾirbɪ9rCEWOi[OZ!it=riLJ{Z#wKHǝoڂQ@_39<+|)꠱sL'dU5=%ʷ!mG@|2ybddX&7?!h>i;?X^zN투fy[f|HGd[[wE;~LVɚeʮxUV,iw^~=!8Gw|5ͩp $MUI9ƛgautv⑚ɝ[VEe *-h99w0(LqF1㝨gN| gc[h;IM^݈G*&}@>qH]D<#y eJ9EysRDoM_],ܴZzpiW~AN9d1KFM4pvj=4 quedl<-}+0xogӸ :惏c阜&cTC9SnSI|2\-D|CƁVN;y bb1%( !. y6hW=ya΍6v$]X)wn}Xvckb 3mP)'VW;aRX5d|,/bBS#uI%Ϝb|tX/`k'r +7VQ,㘜e&l1Vj|j6X)||ŰDy6&>tt/E["l97\Ύ؆vLnN Ux@z;W?!(?x\GV;<+n\>#oEtydNp/~7~呛n7&23 ޡA{ +oe.<4MnwDJʃwmsǾn_c*IīZ.-G_dO?~X~TV.k<+U"q$>/|V.;-'!2(0ȔT-JUI`f|<#~(iIלMF:a<ۢ&<@喏<,xR,n]+p 'Eg_믗K+p"Y@NV8`'CG+ϯ䵝]Rt>NJm $sL4,=qV>h|{x'rs%4GZ08kƫFr>=(ݲL.4z=_9Ƨ'D4Vȓ߇Ƶq4%-F/zA~w{enk|SS*Ǵ'8XzL470 +q2JRؤ` 3)ےj/W,Vɠiu,R|*lr\H:Jfb9_BΡ!)BdpM Q{/ƊlFާaB z˞u:RQX%l%h` ZJmbnb]CX$#qt<$Y4fڐWmupl!z e +-w|,m1BٟA)<7ӤXS(YbX}TOǕ!,Pd+d=cw2<1]sQU_9tA~V1׬'%8WY ϝ].#x7aشEvN7,׮F7XS'ueGu5|+9@Dõv9^U!_?}d2<( ::DH^G6_ mK8w/YoZ#s5{ޑpq*X]2ٴJVm^!\W=A8xny3f\wе-mKѨFY jӄ݃-_oGn]*ouHΏUfXpXp9Y4TU}Զ㕸3p\;$xBƆq4OevK-FIY&'G3OY؆$}H'du[ҋ2چwZ}N58 *"%w8h$;~!GʡDtC2+Ϗ(/6hĭkg{,N-eby66JE1vˤhL;:9ў6}qa,#q8oh7b6G5d)b,؀6 XfAT;|U5sh6y|\qb,ikg#`); }4F 6H6ISϰ&G[rHs,m/a/'|fIl,)=>Ṿx|vhl 1 X̵ 8^M1_= Ţ&:c,#9-s- ,gmج6>39w b>nXgٵFXb#W]LY>,#X~* HcyBPyyV.%:avCK5Mhc1>o|] MtFݤ6Gav% M,MJ{9 }BErwxx#RӱA\#U#g5|/bKNո=nq |vT>R%skkl}1MLRUVrͧ~rܨ\~yKv|臟 `q`#8=fx5uu8An`tn<^gyjOѠ-~Du-&edg6J3g%?o*F$WYgU"rWQ1$fv0`(E8ZS_Vtki?}i?)[oJ浵ʖ+7l^SWqί0+B>$kQ$-8q2o+YCzµP:ɩxIw&fƗ?+?T/X g[ҋ+&i—4fƳxo/~Qn3Ȩ,F'4&zbؔh8a˂EOo"HC\L\5:zrLΟǾr-zGOʡfYNϩm\82W5$_I&Fq҈]<sF{Q>˙Q#1ǚ.nKnڰD^{89I> _=wJ?"s[kP+ cr'66*C8E8X7VR;a vU{k+7X9r '/-VԱ{Z9 _AJWJWOP hKٯʍeد4VVkIq$Ŝ]`M91RRhv7KWw^'r\maΧ%ߵ)>`FyPz|UQ>E?Rh&JigS.('^eŤl1/m|ϱ8c(u1/iDTbyֆnev(65PXFS ;[ IxdxP[Ӝr`Nv/nc9xPu 8/5WMrxYc[u%]Q:s,Y^xU6 5b7]!#gO;Nὅy'">hy<$#3p7^=<.C\ɕuҟGُ$>T(>&܃xWץv 8):d嚕X{EU#+ұt^~D<_:̕ndd21,#DˆPn/l|'cW̗۞[?)goZ+;KA0.[w#&"{?[ 1#_ GYr4N>/s w+_2~VGn@pw:j`/˞Ii+{k+OTONѬ3#|Zϯ7w+'*;r" q1V|2'd=r-.urj׏E98 q∯AIǮcp;[㶋vB=ܰK*;ł s_`lM+f:H;cžh`)>4bbsIa491h|̋ZѸOYJmIrlG׉fM:R6>@X }m١Pv3oK^3;7&M٠ Eh̦XT#\>o7S]eRiY1]Ik׊kg>6xױƟަ6V}V)>5&eLzǎ4ı*/ҩq} a9_Gs|Z';޹Znz|o"p>)^܉qN'sX#w1JKCSO<$OuT^| y?W|{ٷgT2T;/[7ˆaG#/FKo+T4ɺÚ7_{N77 i[wFg_|W .xIqbV3DJ=5^<8@pqSwJpǀ\?bj|Ctjw,Xq5(NX- %Փ\m Jik[oL d2^+>$;S8[Ød'mWȗ{X㝂:˿/ ܵمOľzrXZ+pgjٸq,DZrױ}REΞO~r kܱ2oVۥp?kkloWs+;e TcjF=t񽕾^Yڰa^cs''3R+_矑 +kR;?&7=&qk$کɇ3]IDATg]q۱GԚLK5em܅ycYv16sM습|Xl.hn|>FY 3bŸ{?*Cw>qذxaMܹy?J'㕎եO+8ttαWr*S.7V..ڇ8p)ҁ% k_hc%&I ~,N-WtrhN͟b,oql6` |lw?v?\LFF$eݯ9O 6R;<2qՊ+8?]u,믿V={OYt!i'wMZJy҂Xc~ +O?7#4wZ4 .?\%7\D㣕2~{q˖Wd|P>҉C 2Ԁ?NʫoAphN"Y`T+/7fn6aمXX%ț8VƫCu8 K>OˆqGVŠ+o*uod.Nwǝq^3xٹ|p/I3>Sl<3 MK_"h|k\'۟[2o.pE9\YgzƤvA N&20x+U2x|ڤ <Υo㽑O8X{{psMuknjb؆;L, 0\hF**j~` e19 ݮmblyZ.Y;gV{+\XYf%Vq`)_&qLvTuuP a1g=CveͿWWH^Vb++磆_Xqlߏإ܄JRꥌ1 L}![NkJi{R HcYn݇g+3hG!;1UioxA:\:#僛Ɂ]^(Y{S;j7rEk? tRW`?rp!]ɚ_AIǣ4Bn+OzeH]v&5ؓ/}5h p e~ʁ/%XIv^p{1&s<Յ ;ƙ?!WuGxZAxBkhdl5Z id_H18/ro3 6|u)J;02}q%}}27TPc8N>1i5&* |薲cŰ+>cT/H{JdG˝|8rl) bnSk60 hq41féJ)N,ȗz_ QP{4>S*=/+Gh/&ҭb++=nn.k?|Ȯj'|*]  j_%[^+/bgSD8ZO9qEN0t4+ubWΟ&ur}70LֹCӄ2$CM\E6`D6f`yo2BJ0W46Qc>S˼ Ńwg/0/,3Yȭd6_Yr) Z勱Xfr+|4,Qwy)t:DvjB6ñԟ&+=hLZ QjƠE.|lTT;;-,=~Y>X{pHEv3K`byvK e1wo70> i(‚sgX%س#h|O9-$9 u|lAJ{pJ4b9dZJ5؜&K[CM%xXE uפ] -ʜ{?WyC=4dh~ ƊBQ%pKǩ}ԁMNk s^\3xܷsXmxX!xÏϤ&^Qu>J2:2"<(%iEܑgqVɕ0ȰQ$HOٱ9z&u%mۚnzh唦"6֜J_6bCYKbڐ߸UGդ \/m v% ,VXGeR8ACa)O{LB)U&ĀXHɗ 涺X]%WV ɆK+/s;@ҤE3Ol0?-*3%6_/;')LVw(j  hz',]Kq8lWanlG!\Y ~o$ 1zO/אh,g GM5\;^ͱc-xA #\T-<2b{ϕYX܆9O2jlxR>>>2:_R c93 ̜߃]6a3|36|/)CRI9q|Ncў;ټc <=َUU}7ƣܼc x|Yo{+97xQw"/S=E}jv JSw0 {4ӼvQa;ڙc+PJlb`dT,ȗ@O$VKޙ_줚{;ٝ$K$Yġ$hu{s>/hfn4N~x'?Y;֍X&i;EcKa>L9+ &㛎/iOEŊH6r[S<~6b5h1DY|W+U_V$x=Egn:nS˵3Ɨ X>'Sxna9}.an7&=yáhƤuӌ&d1xfX7K3f/cgVsY,Fݯb{O+0q5y}<̡8گ}"QXCu^&Qt1DEHH>f/oLr'>ѺY)Ηg࣓).S i:|s~f0y\[,)Uln6ׅК/#WSnXOc (ВqgLbƅXT!/$VoHiBڳ+=hXY h nㅹ1߂|˙wb尮v,]1;6lIX4Fw٠DP]ۼrf3z?Yv16N9R|łO$Z$V*>&ObU5ypO˙M(@v2i+i~kW||tiphDOu䣪< :U4r{Wz9zoqM֧Xn@mLhM UUr+e~%dTL̞:r#3?βb M\yh('=ۘ6>s:T; H1bQ+ jnY>v%4>stեFi>a; ]c1 ڼ+棑95V4@bZ$ }IMm̞eŪ\>ݸqG}ig휇),#X%&9u0>1PTX@gmwHp>oմxQئ? 4@|((ۨaXLSrdpm󱎤OҤv>oWS3b0XbL|htvX5fGrLڙ }٤,R2X%yvi"|(v!VQ͎&XiCbF "ܗRWӡbe9>Q[`죣n\yhKC˵H>XfrΗȵ)m9J)b9+6aLjsˋb>b>y? Jر)εZQp, M jC{ڸQǕY\M_/GQOb9@~8 3: >jw~XIa .5-?&j*a 4EQs;qj:f 1zRC{"A)yNa\a?Ƨ4,勰f+*/ሁ˴}X.KYw>6kH!m 2iF2",⤰W Km iDXbc|n <,4LSX^cQX4Cl0<]hv]8BbFCدTF٦s.1Y 'F,+ب)̅ytvО>ڹ ruNǂIb<bҏlI~UӬb>?hvڨeદs,E8nۗŠqnjRDٔZѱSR;m(M+t gX%LHf-VE++6%+Q0Vh8),V'm?Wd" 4s e{IENDB`python-nubia-0.2.3/example/000077500000000000000000000000001434345131700155715ustar00rootroot00000000000000python-nubia-0.2.3/example/__init__.py000066400000000000000000000003501434345131700177000ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/example/commands/000077500000000000000000000000001434345131700173725ustar00rootroot00000000000000python-nubia-0.2.3/example/commands/__init__.py000066400000000000000000000003501434345131700215010ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/example/commands/more/000077500000000000000000000000001434345131700203345ustar00rootroot00000000000000python-nubia-0.2.3/example/commands/more/__init__.py000066400000000000000000000000001434345131700224330ustar00rootroot00000000000000python-nubia-0.2.3/example/commands/more/moar_commands.py000066400000000000000000000005341434345131700235270ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia import command @command def another_command(): "Just a simple do nothing command" return None python-nubia-0.2.3/example/commands/sample_commands.py000066400000000000000000000055411434345131700231130ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import asyncio import socket import typing from termcolor import cprint from nubia import argument, command, context @command(aliases=["lookup"]) @argument("hosts", description="Hostnames to resolve", aliases=["i"]) @argument("bad_name", name="nice", description="testing") def lookup_hosts(hosts: typing.List[str], bad_name: int): """ This will lookup the hostnames and print the corresponding IP addresses """ ctx = context.get_context() cprint("Input: {}".format(hosts), "yellow") cprint("Verbose? {}".format(ctx.verbose), "yellow") for host in hosts: cprint("{} is {}".format(host, socket.gethostbyname(host))) # optional, by default it's 0 return 0 @command("good-name") def bad_name(): """ This command has a bad function name, but we ask Nubia to register a nicer name instead """ cprint("Good Name!", "green") @command("async-good-name") async def async_bad_name(): """ This command has a bad function name, but we ask Nubia to register a nicer name instead """ cprint("This is async!", "green") @command @argument("number", type=int) async def triple(number): "Calculates the triple of the input value" cprint("Input is {}".format(number)) cprint("Type of input is {}".format(type(number))) cprint("{} * 3 = {}".format(number, number * 3)) await asyncio.sleep(2) @command("be-blocked") def be_blocked(): """ This command is an example of command that blocked in configerator. """ cprint("If you see me, something is wrong, Bzzz", "red") @command @argument("style", description="Pick a style", choices=["test", "toast", "toad"]) @argument("stuff", description="more colors", choices=["red", "green", "blue"]) @argument("code", description="Color code", choices=[12, 13, 14]) def pick(style: str, stuff: typing.List[str], code: int): """ A style picking tool """ cprint("Style is '{}' code is {}".format(style, code), "yellow") # instead of replacing _ we rely on camelcase to - super-command @command class SuperCommand: "This is a super command" def __init__(self, shared: int = 0) -> None: self._shared = shared @property def shared(self) -> int: return self._shared """This is the super command help""" @command @argument("firstname", positional=True) def print_name(self, firstname: str): """ print a name """ cprint("My name is: {}".format(firstname)) @command(aliases=["do"]) def do_stuff(self, stuff: int): """ doing stuff """ cprint("stuff={}, shared={}".format(stuff, self.shared)) python-nubia-0.2.3/example/nubia_context.py000066400000000000000000000016551434345131700210140ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia import context, eventbus, exceptions class NubiaExampleContext(context.Context): def on_connected(self, *args, **kwargs): pass async def on_cli(self, cmd, args): # dispatch the on connected message self.verbose = args.verbose await self.registry.dispatch_message(eventbus.Message.CONNECTED) async def on_interactive(self, args): self.verbose = args.verbose ret = await self._registry.find_command("connect").run_cli(args) if ret: raise exceptions.CommandError("Failed starting interactive mode") # dispatch the on connected message await self.registry.dispatch_message(eventbus.Message.CONNECTED) python-nubia-0.2.3/example/nubia_example.py000066400000000000000000000012301434345131700207500ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import sys from nubia_plugin import NubiaExamplePlugin import example.commands from nubia import Nubia, Options if __name__ == "__main__": plugin = NubiaExamplePlugin() shell = Nubia( name="nubia_example", command_pkgs=example.commands, plugin=plugin, options=Options( persistent_history=False, auto_execute_single_suggestions=False ), ) sys.exit(shell.run()) python-nubia-0.2.3/example/nubia_plugin.py000066400000000000000000000064161434345131700206260ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse from nubia_context import NubiaExampleContext from nubia_statusbar import NubiaExampleStatusBar from nubia import CompletionDataSource, PluginInterface from nubia.internal.blackcmd import CommandBlacklist class NubiaExamplePlugin(PluginInterface): """ The PluginInterface class is a way to customize nubia for every customer use case. It allowes custom argument validation, control over command loading, custom context objects, and much more. """ def create_context(self): """ Must create an object that inherits from `Context` parent class. The plugin can return a custom context but it has to inherit from the correct parent class. """ return NubiaExampleContext() def validate_args(self, args): """ This will be executed when starting nubia, the args passed is a dict-like object that contains the argparse result after parsing the command line arguments. The plugin can choose to update the context with the values, and/or decide to raise `ArgsValidationError` with the error message. """ pass def get_opts_parser(self, add_help=True): """ Builds the ArgumentParser that will be passed to , use this to build your list of arguments that you want for your shell. """ opts_parser = argparse.ArgumentParser( description="Nubia Example Utility", formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=add_help, ) opts_parser.add_argument( "--config", "-c", default="", type=str, help="Configuration File" ) opts_parser.add_argument( "--verbose", "-v", action="count", default=0, help="Increase verbosity, can be specified multiple times", ) opts_parser.add_argument( "--stderr", "-s", action="store_true", help="By default the logging output goes to a " "temporary file. This disables this feature " "by sending the logging output to stderr", ) return opts_parser def get_completion_datasource_for_global_argument(self, argument): if argument == "--config": return ConfigFileCompletionDataSource() return None def create_usage_logger(self, context): """ Override this and return you own usage logger. Must be a subtype of UsageLoggerInterface. """ return None def get_status_bar(self, context): """ This returns the StatusBar object that handles the bottom status bar and the right-side per-line status """ return NubiaExampleStatusBar(context) def getBlacklistPlugin(self): blacklister = CommandBlacklist() blacklister.add_blocked_command("be-blocked") return blacklister class ConfigFileCompletionDataSource(CompletionDataSource): def get_all(self): return ["/tmp/c1", "/tmp/c2"] python-nubia-0.2.3/example/nubia_statusbar.py000066400000000000000000000020251434345131700213300ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from pygments.token import Token from nubia import context, statusbar class NubiaExampleStatusBar(statusbar.StatusBar): def __init__(self, context): self._last_status = None def get_rprompt_tokens(self): if self._last_status: return [(Token.RPrompt, "Error: {}".format(self._last_status))] return [] def set_last_command_status(self, status): self._last_status = status def get_tokens(self): spacer = (Token.Spacer, " ") if context.get_context().verbose: is_verbose = (Token.Warn, "ON") else: is_verbose = (Token.Info, "OFF") return [ (Token.Toolbar, "Hello!"), spacer, (Token.Toolbar, "Verbose "), spacer, is_verbose, ] python-nubia-0.2.3/install.sh000066400000000000000000000003661434345131700161450ustar00rootroot00000000000000#!/bin/bash # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. python3 setup.py install python-nubia-0.2.3/nubia/000077500000000000000000000000001434345131700152345ustar00rootroot00000000000000python-nubia-0.2.3/nubia/__init__.py000066400000000000000000000016141434345131700173470ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from .internal import context, exceptions from .internal.deprecation import deprecated from .internal.io import eventbus from .internal.io.session_logger import SessionLogger from .internal.nubia import Nubia from .internal.options import Options from .internal.plugin_interface import CompletionDataSource, PluginInterface from .internal.typing import argument, command from .internal.ui import statusbar name = "nubia" __all__ = [ "CompletionDataSource", "Nubia", "Options", "PluginInterface", "SessionLogger", "argument", "command", "context", "deprecated", "eventbus", "exceptions", "statusbar", ] __version__ = "0.2.3" python-nubia-0.2.3/nubia/internal/000077500000000000000000000000001434345131700170505ustar00rootroot00000000000000python-nubia-0.2.3/nubia/internal/__init__.py000066400000000000000000000003501434345131700211570ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/nubia/internal/blackcmd.py000066400000000000000000000010721434345131700211620ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # class CommandBlacklist: _blacklisted_commands = {} def __init__(self): # Ovveride this funtion pass def is_blacklisted(self, command): # Overide this return command in self._blacklisted_commands def add_blocked_command(self, command): self._blacklisted_commands[command] = "" python-nubia-0.2.3/nubia/internal/cmdbase.py000066400000000000000000000475411434345131700210330ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import asyncio import copy import inspect import sys import traceback from collections import OrderedDict from textwrap import dedent from typing import Iterable, Optional, Callable from prompt_toolkit.completion import CompleteEvent, Completion, WordCompleter from prompt_toolkit.document import Document from termcolor import cprint from nubia.internal import parser from nubia.internal.completion import AutoCommandCompletion from nubia.internal.exceptions import CommandParseError from nubia.internal.helpers import ( find_approx, function_to_str, suggestions_msg, try_await, ) from nubia.internal.options import Options from nubia.internal.typing import FunctionInspection, inspect_object from nubia.internal.typing.argparse import ( get_arguments_for_command, get_arguments_for_inspection, register_command, ) from nubia.internal.typing.builder import apply_typing from nubia.internal.typing.inspect import is_list_type from . import context class Command: """A Command is the abstraction over one or more commands that will executed by the shell, A Command sub-class must implement `cmds` with a dict that maps command to a description. """ def __init__(self): self._command_registry = None self._built_in = False @property def built_in(self) -> bool: return self._built_in def set_command_registry(self, command_registry): self._command_registry = command_registry async def run_interactive(self, cmd, args, raw): """ This function MUST be overridden by all commands. It will be called when the command is executed in interactive mode. """ raise NotImplementedError("run_interactive must be overridden") async def run_cli(self, args): """ This function SHOULD be implemented in order to expose a subcommand in the CLI interface. It will be called when run from the CLI. """ pass async def add_arguments(self, parser): """ This function receives an instance of an "argparse.ArgumentParser". Every command SHOULD use it to tell the CLI interface which options needs. """ # register_command(parser, inspect_object(self._fn)) pass @property def metadata(self) -> FunctionInspection: """ Returns the command specification as an instance of FunctionInspection object. This is used to generate a completion model for external completers """ return {} def get_completions(self, cmd, document, complete_event) -> Iterable[Completion]: """ This function SHOULD be implemented to feed the interactive auto completion of command arguments. Example: auto complete the available tables in the "describe" query command. """ return [] def get_command_names(self): """ This function MUST be implemented to tell the framework which commands this module implements. Must return a list of strings. """ raise NotImplementedError("get_command_names must be overridden") def get_cli_aliases(self): """ This function SHOULD be implemented to instruct the command dispatcher about alternative commands available in the CLI. Example: while the "commands/query.py" exports "select" and" describe" in interactive mode, the CLI uses the subcommand "query" to run those commands. Must return a list of strings. """ return [] def get_help(self, cmd, *args): """ This function SHOULD be implemented to show command help when running ':help'. It must return a string associated with the given command. """ pass def get_help_short(self, cmd, *args): """Return a shortened help. This is for example used for interactive autocompletion.""" help = self.get_help(cmd, *args) return help.split("\n", 1)[0] if help else None @property def super_command(self) -> bool: """ Does this command parse sub-commands? """ return False def has_subcommand(self, subcommand) -> bool: """ Does this command have `subcommand` as a valid sub-command? """ return False class AutoCommand(Command): def __init__(self, fn, options: Optional[Options] = None): self._built_in = False self._fn = fn self._options = options or Options() if not callable(fn): raise ValueError("fn argument must be a callable") self._obj_metadata = inspect_object(fn) self._is_super_command = len(self.metadata.subcommands) > 0 self._subcommand_names = [] # We never expect a function to be passed here that has a self argument # In that case, we should get a bound method if "self" in self.metadata.arguments and not inspect.ismethod(self._fn): raise ValueError( "Expecting either a function (eg. bar) or " "a bound method (eg. Foo().bar). " "You passed what appears to be an unbound method " "(eg. Foo.bar) it has a 'self' argument: %s" % function_to_str(fn) ) if not self.metadata.command: raise ValueError( "function or class {} needs to be annotated with " "@command".format(function_to_str(fn)) ) # If this is a super command, we need a completer for sub-commands if self.super_command: self._commands_completer = WordCompleter( [], ignore_case=True, sentence=True ) for _, inspection in self.metadata.subcommands: _sub_name = inspection.command.name self._commands_completer.words.append(_sub_name) self._commands_completer.meta_dict[_sub_name] = dedent( inspection.command.help ).strip() self._subcommand_names.append(_sub_name) @property def metadata(self) -> FunctionInspection: """ The Inspection object of this command. This object contains all the information required by AutoCommand to understand the command arguments type information, help messages, aliases, and attributes. """ return self._obj_metadata def _create_subcommand_obj(self, key_values): """ Instantiates an object of the super command class, passes the right arguments and returns a dict with the remaining unused arguments """ kwargs = { k: v for k, v in get_arguments_for_inspection(self.metadata, key_values).items() if v is not None } remaining = { k: v for k, v in key_values.items() if k.replace("-", "_") not in kwargs.keys() } return self._fn(**kwargs), remaining async def run_interactive(self, cmd, args, raw): try: args_metadata = self.metadata.arguments parsed = parser.parse(args, expect_subcommand=self.super_command) # prepare args dict parsed_dict = parsed.asDict() args_dict = parsed.kv.asDict() key_values = parsed.kv.asDict() command_name = cmd # if this is a super command, we need first to create an instance of # the class (fn) and pass the right arguments if self.super_command: subcommand = parsed_dict.get("__subcommand__") if not subcommand: cprint( "A sub-command must be supplied, valid values: " "{}".format(", ".join(self._get_subcommands())), "red", ) return 2 subcommands = self._get_subcommands() if subcommand not in subcommands: suggestions = find_approx(subcommand, subcommands) if ( len(suggestions) == 1 and self._options.auto_execute_single_suggestions ): print() cprint( "Auto-correcting '{}' to '{}'".format( subcommand, suggestions[0] ), "red", attrs=["bold"], ) subcommand = suggestions[0] else: print() cprint( "Invalid sub-command '{}'{} " "valid sub-commands: {}".format( subcommand, suggestions_msg(suggestions), ", ".join(self._get_subcommands()), ), "red", attrs=["bold"], ) return 2 sub_inspection = self.subcommand_metadata(subcommand) instance, remaining_args = self._create_subcommand_obj(args_dict) assert instance args_dict = remaining_args key_values = copy.copy(args_dict) args_metadata = sub_inspection.arguments attrname = self._find_subcommand_attr(subcommand) command_name = subcommand assert attrname is not None fn = getattr(instance, attrname) else: # not a super-command, use use the function instead fn = self._fn positionals = parsed_dict["positionals"] if parsed.positionals != "" else [] # We only allow positionals for arguments that have positional=True # ِ We filter out the OrderedDict this way to ensure we don't lose the # order of the arguments. We also filter out arguments that have # been passed by name already. The order of the positional arguments # follows the order of the function definition. can_be_positional = self._positional_arguments( args_metadata, args_dict.keys() ) if len(positionals) > len(can_be_positional): if len(can_be_positional) == 0: err = "This command does not support positional arguments" else: # We have more positionals than we should err = ( "This command only supports ({}) positional arguments, " "namely arguments ({}). You have passed {} arguments ({})" " instead!" ).format( len(can_be_positional), ", ".join(can_be_positional.keys()), len(positionals), ", ".join(str(x) for x in positionals), ) cprint(err, "red") return 2 # constuct key_value dict from positional arguments. args_from_positionals = { key: value for value, key in zip(positionals, can_be_positional) } # update the total arguments dict with the positionals args_dict.update(args_from_positionals) # Run some validations on number of arguments provided # do we have keys that are supplied in both positionals and # key_value style? duplicate_keys = set(args_from_positionals.keys()).intersection( set(key_values.keys()) ) if duplicate_keys: cprint( "Arguments '{}' have been passed already, cannot have" " duplicate keys".format(list(duplicate_keys)), "red", ) return 2 # check for verbosity override in kwargs ctx = context.get_context() old_verbose = ctx.args.verbose if "verbose" in args_dict: ctx.set_verbose(args_dict["verbose"]) del args_dict["verbose"] del key_values["verbose"] # do we have keys that we know nothing about? extra_keys = set(args_dict.keys()) - set(args_metadata) if extra_keys: cprint( f"Unknown argument(s) {sorted(extra_keys)} were passed", "magenta", ) return 2 # is there any required keys that were not resolved from positionals # nor key_values? missing_keys = set(args_metadata) - set(args_dict.keys()) if missing_keys: required_missing = [] for key in missing_keys: if not args_metadata[key].default_value_set: required_missing.append(key) if required_missing: cprint( "Missing required argument(s) {} for command" " {}".format(required_missing, command_name), "yellow", ) return 3 # convert expected types for arguments for key, value in args_dict.items(): target_type = args_metadata[key].type if target_type is None: target_type = str try: new_value = apply_typing(value, target_type) except ValueError: fn_name = function_to_str(target_type, False, False) cprint( 'Cannot convert value "{}" to {} on argument {}'.format( value, fn_name, key ), "yellow", ) return 4 else: args_dict[key] = new_value # Validate that arguments with `choices` are supplied with the # acceptable values. We can't validate dynamic completions yet for arg, value in args_dict.items(): choices = args_metadata[arg].choices if choices and not isinstance(choices, Callable): # Validate the choices in the case of values and list of # values. if is_list_type(args_metadata[arg].type): bad_inputs = [v for v in value if v not in choices] if bad_inputs: cprint( f"Argument '{arg}' got an unexpected " f"value(s) '{bad_inputs}'. Expected one " f"or more of {choices}.", "red", ) return 4 elif value not in choices: cprint( f"Argument '{arg}' got an unexpected value " f"'{value}'. Expected one of " f"{choices}.", "red", ) return 4 # arguments appear to be fine, time to run the function try: # convert argument names back to match the function signature args_dict = {args_metadata[k].arg: v for k, v in args_dict.items()} ret = await try_await(fn(**args_dict)) ctx.set_verbose(old_verbose) except Exception as e: cprint("Error running command: {}".format(str(e)), "red") cprint("-" * 60, "yellow") traceback.print_exc(file=sys.stderr) cprint("-" * 60, "yellow") return 1 return ret except CommandParseError as e: cprint("Error parsing command", "red") cprint(cmd + " " + args, "white", attrs=["bold"]) cprint((" " * (e.col + len(cmd))) + "^", "white", attrs=["bold"]) cprint(str(e), "yellow") return 1 def _positional_arguments(self, args_metadata, filter_out): positionals = OrderedDict() for k, v in args_metadata.items(): if v.positional and k not in filter_out: positionals[k] = v return positionals def subcommand_metadata(self, name: str) -> FunctionInspection: assert self.super_command subcommands = self.metadata.subcommands for _, inspection in subcommands: if inspection.command.name == name: return inspection def _find_subcommand_attr(self, name): assert self.super_command subcommands = self.metadata.subcommands for attr, inspection in subcommands: if inspection.command.name == name or name in inspection.command.aliases: return attr # be explicit about returning None for readability return None def _get_subcommands(self) -> Iterable[str]: assert self.super_command return [inspection.command.name for _, inspection in self.metadata.subcommands] def _kwargs_for_fn(self, fn, args): return { k: v for k, v in get_arguments_for_command(fn, args).items() if v is not None } async def run_cli(self, args): # if this is a super-command, we need to dispatch the call to the # correct function kwargs = self._kwargs_for_fn(self._fn, args) try: if self._is_super_command: # let's instantiate an instance of the klass instance = self._fn(**kwargs) # we need to find the actual method we want to call, in addition to # this we need to extract the correct kwargs for this method # find which function it is in the sub commands attrname = self._find_subcommand_attr(args._subcmd) assert attrname is not None fn = getattr(instance, attrname) kwargs = self._kwargs_for_fn(fn, args) else: fn = self._fn ret = await try_await(fn(**kwargs)) return ret except Exception as e: cprint("Error running command: {}".format(str(e)), "red") cprint("-" * 60, "yellow") traceback.print_exc(file=sys.stderr) cprint("-" * 60, "yellow") return 1 @property def super_command(self): return self._is_super_command def has_subcommand(self, subcommand): assert self.super_command return subcommand.lower() in self._subcommand_names async def add_arguments(self, parser): register_command(parser, self.metadata) def get_command_names(self): command = self.metadata.command return [command.name] + command.aliases def get_completions( self, _: str, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: if self._is_super_command: exploded = document.text.lstrip().split(" ", 1) # Are we at the first word? we expect a sub-command here if len(exploded) <= 1: return self._commands_completer.get_completions( document, complete_event ) state_machine = AutoCommandCompletion(self, document, complete_event) return state_machine.get_completions() def get_help(self, cmd, *args): help = self.metadata.command.help return dedent(help).strip() if help else None python-nubia-0.2.3/nubia/internal/cmdloader.py000066400000000000000000000026231434345131700213570ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import pkgutil import types import typing as t def _walk_module(module: types.ModuleType): for attr_name in dir(module): # filter out private members if not attr_name.startswith("_"): member = getattr(module, attr_name) if hasattr(member, "__command"): yield member def _walk_package(name, path) -> t.List[types.FunctionType]: packages = pkgutil.walk_packages(path, prefix=f"{name}.") for importer, modname, ispkg in packages: loaded = importer.find_module(modname).load_module(modname) if not ispkg: yield from _walk_module(loaded) def load_commands(base_package) -> None: """ Loads all commands defined in a loaded python package object. This function recursively look for classes and function annotated with @command and return a list of these objects. """ if base_package is not None: path = None if hasattr(base_package, "__path__"): path = getattr(base_package, "__path__") else: path = getattr(base_package, "__file__") assert path is not None yield from _walk_package(base_package.__name__, path) python-nubia-0.2.3/nubia/internal/commands/000077500000000000000000000000001434345131700206515ustar00rootroot00000000000000python-nubia-0.2.3/nubia/internal/commands/__init__.py000066400000000000000000000003501434345131700227600ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/nubia/internal/commands/builtin.py000066400000000000000000000041671434345131700227010ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia.internal import context from nubia.internal.cmdbase import Command from nubia.internal.interactive import IOLoop from nubia.internal.io.eventbus import Message class Connect(Command): """ A pseudo command that implicitly gets called to start the interactive mode """ cmds = {"connect": "Start the interactive mode"} def __init__(self): super(Connect, self).__init__() self._built_in = True async def run_interactive(self, cmd, args, raw): return await self._run() async def run_cli(self, args): return await self._run() async def _run(self): await self._command_registry.dispatch_message(Message.CONNECTED) return 0 def get_command_names(self): return self.cmds.keys() def add_arguments(self, parser): parser.add_parser("connect") def get_help(self, cmd, *args): return self.cmds[cmd] class Exit(Command): HELP = "Exits the program" cmd = ["quit", "q", "exit"] def __init__(self): super(Exit, self).__init__() self._built_in = True def run_interactive(self, cmd, args, raw): raise EOFError() return 0 def get_command_names(self): return self.cmd def get_help(self, cmd, *args): return self.HELP class Verbose(Command): """ Changes verbosity level in interactive mode """ HELP = "Prints or changes verbosity level, accepts integer or True/False" CMD = ":verbose" def __init__(self): super(Verbose, self).__init__() self._built_in = True def run_interactive(self, cmd, args, raw): ctx = context.get_context() if args: ctx.set_verbose(args) else: print("Current verbosity: {}".format(ctx.args.verbose)) def get_command_names(self): return [self.CMD] def get_help(self, cmd, *args): return self.HELP python-nubia-0.2.3/nubia/internal/commands/help.py000066400000000000000000000044261434345131700221610ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from prettytable import PrettyTable from termcolor import colored, cprint from nubia.internal import context from nubia.internal.cmdbase import Command from nubia.internal.exceptions import CommandError, UnknownCommand class HelpCommand(Command): HELP = "Prints help about all the commands" cmds = {"help": HELP, "?": HELP} def __init__(self): super(Command, self).__init__() self._built_in = True @property def registry(self): return context.get_context().registry def get_completions(self, cmd, document, complete_event): return self.registry.get_completer().get_completions(document, complete_event) async def run_interactive(self, _0, args, _2): if args: args = args.split() try: cmd_instance = self.registry.find_command(args[0]) if not cmd_instance: raise UnknownCommand(f"Command `{args[0]}` is unknown") else: help_msg = cmd_instance.get_help(args[0].lower(), *args) print(help_msg) except CommandError as e: cprint(str(e), "red") return 1 else: built_ins = PrettyTable(["Command", "Description"]) built_ins.align = "l" t = PrettyTable(["Command", "Description"]) t.align = "l" commands = { cmd_name: cmd for cmd in self.registry.get_all_commands() for cmd_name in cmd.get_command_names() } for cmd_name in sorted(commands): cmd = commands[cmd_name] table = built_ins if cmd.built_in else t cmd_help = cmd.get_help(cmd_name) table.add_row([colored(cmd_name, "magenta"), cmd_help]) print(t) cprint("Built-in Commands", "yellow") print(built_ins) return 0 def get_command_names(self): return self.cmds.keys() def get_help(self, cmd, *args): return self.HELP python-nubia-0.2.3/nubia/internal/completion.py000066400000000000000000000303111434345131700215710ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import itertools import logging from typing import TYPE_CHECKING, Iterable, Callable, List import pyparsing as pp from prompt_toolkit.completion import CompleteEvent, Completion from prompt_toolkit.document import Document from nubia.internal import parser from nubia.internal.helpers import function_to_str if TYPE_CHECKING: from nubia.internal.cmdbase import AutoCommand # noqa class TokenParse: """ This class captures an interactive shell token that cannot be fully parser by the interactive shell parser and analyze it. """ def __init__(self, token: str) -> None: self._token = token self._key = "" self._is_argument = False self._is_list = False self._is_dict = False self._last_value = "" self.parse() def parse(self): key, delim, value = self._token.partition("=") # Is everything before the = sane? if any(x in key for x in "[]{}\"'"): # We will treat this as positional in this case return # This is key=value if delim == "=": self._is_argument = True self._key = key else: # This is positional, the value is the key value = self._key assert len(value) == 0 if len(value) > 0: # Let's parse the value, is it a single, list, dict? value = value.strip() if value[0] == "[": self._is_list = True value = value.strip("[") list_values = value.rpartition(",") self._last_value = list_values[len(list_values) - 1].lstrip() elif value[0] == "{": self._is_dict = True else: self._last_value = value @property def is_argument(self) -> bool: return self._is_argument @property def is_positional(self) -> bool: return not self._is_argument # Talks about the type of the value @property def is_list(self) -> bool: return self._is_list @property def is_dict(self) -> bool: return self._is_dict @property def argument_name(self) -> str: assert self._is_argument return self._key def keys(self) -> Iterable[str]: return [] def values(self) -> Iterable[str]: return [] @property def last_value(self) -> str: return self._last_value @property def is_single_value(self) -> bool: return not (self._is_dict or self._is_list) class AutoCommandCompletion: """ This is the interactive completion state machine, it tracks the parsed tokens out of a command input and builds a data model that is used to understand what would be the next natural completion token(s). """ def __init__( self, cmd_obj: "AutoCommand", document: Document, complete_event: CompleteEvent, ) -> None: self.doc = document self.cmd = cmd_obj self.meta = self.cmd.metadata self.event = complete_event # current state def get_completions(self) -> Iterable[Completion]: """ Returns a """ logger = logging.getLogger(f"{type(self).__name__}.get_completions") # This is a rather sad piece of code. Since nubia uses = as # the completion string, when we specify choices for a keyword, and # select the keyword from the autocompletion suggestion by hitting # SPACE, the additional space confuses the autocompleter for the # values associated with the space. For example, if you had state= # as a keyword, and "up" and "down" as possible choices for the keyword # once you've selected state= from the completion list by hitting SPACE # up and down pop up as possible choices, but typing the first letter # of a choice such as u(for up), doesn't trim the selection to those # starting with u. Selection just stops because the parser gets confsed # about the space after "keyword= " and doesn't provide any more auto # complete suggestions. The lines that follow fix this by removing the # space after = from the text, allowing the parser to do its job. if self.doc.char_before_cursor != " ": pos = self.doc.find_backwards('= ') if pos: spos = self.doc.cursor_position + pos + 1 epos = self.doc.cursor_position + pos + 2 self.doc._text = self.doc._text[:spos] + self.doc._text[epos:] self.doc._cursor_position -= 1 remaining = None try: parsed = parser.parse( self.doc.text, expect_subcommand=self.cmd.super_command ) except parser.CommandParseError as e: parsed = e.partial_result remaining = e.remaining # This is a funky but reliable way to figure that last token we are # interested in manually parsing, This will return the last key=value # including if the value is a 'value', [list], or {dict} or combination # of these. This also matches positional arguments. if self.doc.char_before_cursor in " ]}": last_token = "" else: last_space = self.doc.find_backwards( " ", in_current_line=True) or -1 last_token = self.doc.text[(last_space + 1):] # noqa # We pick the bigger match here. The reason we want to look into # remaining is to capture the state that we are in an open list, # dictionary, or any other value that may have spaces in it but fails # parsing (yet). if remaining and len(remaining) > len(last_token): last_token = remaining try: return self._prepare_args_completions( parsed_command=parsed, last_token=last_token ) except Exception as e: logger.exception(str(e)) return [] def _prepare_args_completions( self, parsed_command: pp.ParseResults, last_token ) -> Iterable[Completion]: assert parsed_command is not None args_meta = self.meta.arguments.values() subcommand = None # are we expecting a sub command? if self.cmd.super_command: # We have a sub-command (supposedly) subcommand = parsed_command.get("__subcommand__") assert subcommand sub_meta = self.cmd.subcommand_metadata(subcommand) if not sub_meta: logging.debug("Parsing unknown sub-command failed!") return [] # we did find the sub-command, yay! # In this case we chain the arguments from super and the # sub-command together args_meta = itertools.chain(args_meta, sub_meta.arguments.values()) # Now let's see if we can figure which argument we are talking about args_meta = self._filter_arguments_by_prefix(last_token, args_meta) # Which arguments did we fully parse already? let's avoid printing them # in completions parsed_keys = parsed_command.asDict().get("kv", []) # We are either completing an argument name, argument value, or # positional value. # Dissect the last_token and figure what is the right completion parsed_token = TokenParse(last_token) ret = [] if parsed_token.is_positional: # TODO: Handle positional argument completions too # To figure which positional we are in right now, we need to run the # same logic that figures if all required arguments has been # supplied and how many positionals have been processed and which # one is next. # This code is already in cmdbase.py run_interactive but needs to be # refactored to be reusable here. pass elif parsed_token.is_argument: argument_name = parsed_token.argument_name arg = self._find_argument_by_name(argument_name) if not arg or arg.choices in [False, None]: return [] # TODO: Support dictionary keys/named tuples completion if parsed_token.is_dict: return [] # We are completing a value, in this case, we need to get the last # meaninful piece of the token `x=[Tr` => `Tr` if isinstance(arg.choices, Callable): choices = arg.choices( self.cmd.metadata.command.name, subcommand, last_token, self.doc.text) if not isinstance(choices, List): raise ValueError('autocomplete function MUST provide list of strings' f', got {choices}') else: if parsed_token.last_value: choices = [c for c in arg.choices if str(c).startswith(parsed_token.last_value)] else: choices = arg.choices ret = [ Completion( text=str(choice), start_position=-len(parsed_token.last_value), ) for choice in choices[:self.cmd._options.limit_visible_choices] ] return ret # We are completing arguments, or positionals. # TODO: We would like to only show positional choices if we exhaust all # required arguments. This will make it easier for the user to figure # that there are still required named arguments. After that point we # will show optional arguments and positionals as possible completions ret = [ Completion( text=arg_meta.name + "=", start_position=-len(last_token), display_meta=self._get_arg_help(arg_meta), ) for arg_meta in args_meta if arg_meta.name not in parsed_keys ] return ret def _filter_arguments_by_prefix(self, prefix: str, arguments=None): arguments = arguments or self.meta.arguments.values() if prefix: return [ arg_meta for arg_meta in arguments if arg_meta.name.startswith(prefix) ] return arguments def _prepare_value_completions(self, prefix, partial_result): parsed_keys = map(lambda x: x[0], partial_result.get("kv", [])) argument, rest = prefix.split("=", 1) arguments = self._filter_arguments_by_prefix(argument) if len(arguments) < 1: return [] if len(arguments) == 1: argument_obj = self._find_argument_by_name(argument) assert argument_obj # was that argument used before? if argument in parsed_keys: logging.debug( "Argument {} was used already, not generating " "completions".format(argument) ) return [] return [] def _find_argument_by_name(self, name): args_meta = list(self.meta.arguments.values()) if self.cmd.super_command: # We need to get the subcommand name subcommand_name = self.doc.text.split(" ")[0] for _, sub in self.meta.subcommands: if sub.command.name == subcommand_name: args_meta.extend(list(sub.arguments.values())) filtered = filter(lambda arg: arg.name == name, args_meta) return next(filtered, None) def _get_arg_help(self, arg_meta): sb = ["["] if arg_meta.type: sb.append(function_to_str(arg_meta.type, False, False)) sb.append(", ") if arg_meta.default_value_set: sb.append("default: ") sb.append(arg_meta.default_value) else: sb.append("required") sb.append("] ") sb.append( arg_meta.description if arg_meta.description else "" ) return "".join(str(item) for item in sb) python-nubia-0.2.3/nubia/internal/constants.py000066400000000000000000000004451434345131700214410ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # DEFAULT_CLIENT_TIMEOUT = 240 DEFAULT_COMMAND_TIMEOUT = 120 python-nubia-0.2.3/nubia/internal/context.py000066400000000000000000000054671434345131700211220ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import copy import getpass import os import sys from threading import RLock from typing import List, Tuple, Any from nubia.internal.io.eventbus import Listener from pygments.token import Token class Context(Listener): def __init__(self): self._binary_name = None self._lock = RLock() self._testing = None self._registry = None self._args = {} def set_binary_name(self, name): self._binary_name = name def set_testing(self, testing): with self._lock: self._testing = testing def set_registry(self, registry): with self._lock: self._registry = registry def set_args(self, args): with self._lock: self._args = copy.deepcopy(args) def set_verbose(self, raw_value): """ Accepts verbosity as int or True/False """ try: value = int(raw_value) except ValueError: value = int(raw_value.lower() == "true") with self._lock: self._args.verbose = value @property def binary_name(self): return self._binary_name @property def testing(self): with self._lock: return self._testing @property def registry(self): with self._lock: return self._registry @property def args(self): with self._lock: return self._args @property def isatty(self): return os.isatty(sys.stdin.fileno()) def get_prompt_tokens(self) -> List[Tuple[Any, str]]: """ Override this and return your own prompt for interactive mode. Expected to return a list of pygments Token tuples. """ tokens = [ (Token.Username, getpass.getuser()), (Token.Colon, ""), (Token.Pound, "> "), ] return tokens async def on_connected(self, *args, **kwargs): """ A callback that gets called when the shell is started cli-mode, the args argument contains the ArgumentParser result. """ pass async def on_interactive(self, args): """ A callback that gets called when the shell is started interactive-mode, the args argument contains the ArgumentParser result. """ pass async def on_cli(self, cmd, args): """ A callback that gets called when the shell is started cli-mode, the args argument contains the ArgumentParser result. """ pass # This is set by LDShell class on constructor. _ctx = None def get_context(): return _ctx python-nubia-0.2.3/nubia/internal/deprecation.py000066400000000000000000000026131434345131700217210ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from functools import wraps from typing import Any, Dict, Optional from termcolor import cprint from nubia.internal.typing import inspect_object def deprecated(message: Optional[str] = None, superseded_by: Optional[str] = None): def decorator(command): @wraps(command) def wrapper(*args: Any, **kwargs: Dict[str, Any]): warning: str = ( "[WARNING] The `{command}` command is deprecated " "and will be eventually removed".format( command=inspect_object(command).command.name ) ) cprint(warning, "yellow") if message is not None: cprint(message, "yellow") elif superseded_by is not None: cprint("Use `{}` command instead".format(superseded_by), "yellow") else: assert False, "Unreachable" return command(*args, **kwargs) wrapper.__doc__ = "[DEPRECATED]" + command.__doc__ return wrapper if not ((message is None) ^ (superseded_by is None)): raise ValueError("Either `message` or `superseded_by` should be used") return decorator python-nubia-0.2.3/nubia/internal/exceptions.py000066400000000000000000000006411434345131700216040ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # class CommandParseError(Exception): pass class CommandError(Exception): pass class UnknownCommand(CommandError): pass class ArgsValidationError(Exception): pass python-nubia-0.2.3/nubia/internal/helpers.py000066400000000000000000000154351434345131700210740ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import inspect import re import signal import string import subprocess from collections import namedtuple from typing import Iterable, Optional import jellyfish def add_command_arguments(parser, options): for option, extras in options.items(): parser.add_argument("--{}".format(option), **extras) async def try_await(result): """ Await if awaitable, otherwise return. """ if inspect.isawaitable(result): return await result return result def run_process(process_arg_list, on_interrupt=None, working_dir=None): """ This runs a process using subprocess python module but handles SIGINT properly. In case we received SIGINT (Ctrl+C) we will send a SIGTERM to terminate the subprocess and call the supplied callback. @param process_arg_list Is the list you would send to subprocess.Popen() @param on_interrupt Is a python callable that will be called in case we received SIGINT This may raise OSError if the command doesn't exist. @return the return code of this process after completion """ assert isinstance(process_arg_list, list) old_handler = signal.getsignal(signal.SIGINT) process = subprocess.Popen(process_arg_list, cwd=working_dir) def handler(signum, frame): process.send_signal(signal.SIGTERM) # call the interrupted callack if on_interrupt: on_interrupt() # register the signal handler signal.signal(signal.SIGINT, handler) rv = process.wait() # after the process terminates, restore the original SIGINT handler # whatever it was. signal.signal(signal.SIGINT, old_handler) return rv FullArgSpec = namedtuple( "FullArgSpec", ( "args", "varargs", "varkw", "defaults", "kwonlyargs", "kwonlydefaults", "annotations", ), ) def get_arg_spec(function): """ Basic backport of python's 3 inspect.gefullargspec to python 2 """ def set_default_value(dictionary, key, value): if not dictionary.get(key, None): dictionary[key] = value if hasattr(inspect, "getfullargspec"): argspec = inspect.getfullargspec(function)._asdict() argspec["annotations"].update(getattr(function, "__annotations__", {})) else: argspec = inspect.getargspec(function)._asdict() # python 3 renamed keywords for varkw argspec["varkw"] = argspec.pop("keywords") argspec["annotations"] = getattr(function, "__annotations__", None) for field in ["args", "defaults", "kwonlyargs"]: set_default_value(argspec, field, []) for field in ["kwonlydefaults", "annotations"]: set_default_value(argspec, field, {}) return FullArgSpec(**argspec) def get_kwargs_for_function(function, **kwargs): arg_spec = get_arg_spec(function) return ( dict(kwargs) if arg_spec.varkw else {k: v for k, v in kwargs.items() if k in arg_spec.args} ) def function_to_str(function, with_module=True, with_args=True): """ Returns a nice string representation of a function """ string = getattr(function, "__name__", str(function)) if with_module: string = "{}.{}".format(function.__module__, string) if with_args: argspec = get_arg_spec(function) args_string = ", ".join(argspec.args) if argspec.varargs: args_string = "{}, *{}".format(args_string, argspec.varargs) if argspec.varkw: args_string = "{}, **{}".format(args_string, argspec.varkw) string = "{}({})".format(string, args_string) return string def transform_name(name, from_char="_", to_char="-"): """ Transforms a symbol from code into something more user friendly For instance: _foo_bar => foo-bar __special__ => special """ name = name.strip() # transforms one or more underscores into dashes. Also remove any # trailing or leading one # e.g, some__very___special -> some-very-special name = re.sub(r"{}+".format(re.escape(from_char)), to_char, name) name = re.sub(r"^{c}|{c}$".format(c=re.escape(to_char)), "", name) if not name: raise ValueError('Invalid name "{}"'.format(name)) return name def transform_class_name(name): """ Tranforms a camel-case class name into dashed name. This also swaps underscores if exists """ new_name = transform_name(name) res = [] for c in new_name: if c in string.ascii_uppercase and len(res) > 0: res.append("-") res.append(c.lower()) else: res.append(c.lower()) return "".join(res) # TypeError. In this case the object is clearly not a subclass, so we # override this behavior for returning False def issubclass_(obj, class_): try: return issubclass(obj, class_) except (AttributeError, TypeError): return False def catchall(func, *args): """ Run the given function with the given arguments, and make sure it never crashes. Note: This still allows some BaseExceptions, like SystemExit and KeyboardInterrupt """ try: func(*args) except Exception as e: print("Error logging to scuba: {}".format(str(e))) def find_approx(cmd_input: str, cmd_map: Optional[Iterable[str]]) -> Iterable[str]: """Finds the closest command to the passed cmd, this is used in case we cannot find an exact match for the cmd We will use two methods, unique prefix match and levenshtein distance match """ prefix_suggestions = set() levenshtein_suggestions = {} for another_command in cmd_map: if str(another_command).startswith(str(cmd_input).lower()): prefix_suggestions.add(another_command) # removing single letter levenshtein suggestions # such as `?`, `q` etc elif len(another_command) > 1: distance = jellyfish.damerau_levenshtein_distance( str(cmd_input).lower(), another_command ) if distance <= 2: levenshtein_suggestions.update({another_command: distance}) if prefix_suggestions: return sorted(prefix_suggestions) else: # sort suggestions by levenshtein distance and then by name return [ k for k, _ in sorted( levenshtein_suggestions.items(), key=lambda i: (i[1], i[0]) ) ] def suggestions_msg(suggestions: Optional[Iterable[str]]) -> str: if not suggestions: return "" else: return f", Did you mean {', '.join(suggestions[:-1])} or {suggestions[-1]}?" python-nubia-0.2.3/nubia/internal/interactive.py000066400000000000000000000171501434345131700217430ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import logging import os from typing import Any, List, Tuple from prompt_toolkit import PromptSession from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import Completer from prompt_toolkit.document import Document from prompt_toolkit.enums import EditingMode from prompt_toolkit.formatted_text import PygmentsTokens from prompt_toolkit.history import FileHistory, InMemoryHistory from prompt_toolkit.layout.processors import HighlightMatchingBracketProcessor from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.patch_stdout import patch_stdout from termcolor import cprint from nubia.internal.helpers import catchall, find_approx, suggestions_msg, try_await from nubia.internal.io.eventbus import Listener from nubia.internal.options import Options from nubia.internal.ui.lexer import NubiaLexer from nubia.internal.ui.style import shell_style def split_command(text): return text.lstrip(" ").split(" ", 1) class IOLoop(Listener): def __init__(self, context, plugin, usagelogger, options: Options): self._ctx = context self._command_registry = self._ctx.registry self._plugin = plugin self._options = options self._blacklist = self._plugin.getBlacklistPlugin() self._status_bar = self._plugin.get_status_bar(context) self._completer = ShellCompleter(self._command_registry) self._command_registry.register_listener(self) self._usagelogger = usagelogger def _build_cli(self): if self._options.persistent_history: history = FileHistory( os.path.join( os.path.expanduser("~"), ".{}_history".format(self._ctx.binary_name) ) ) else: history = InMemoryHistory() # If EDITOR does not exist, take EMACS # if it does, try fit the EMACS/VI pattern using upper editor = getattr( EditingMode, os.environ.get("EDITOR", "EMACS").upper(), EditingMode.EMACS ) return PromptSession( history=history, auto_suggest=AutoSuggestFromHistory(), lexer=PygmentsLexer(NubiaLexer), completer=self._completer, input_processors=[HighlightMatchingBracketProcessor(chars="[](){}")], style=shell_style, bottom_toolbar=self._get_bottom_toolbar, editing_mode=editor, complete_in_thread=True, refresh_interval=1, include_default_pygments_style=False, ) def _get_prompt_tokens(self) -> List[Tuple[Any, str]]: return self._plugin.get_prompt_tokens(self._ctx) def _get_bottom_toolbar(self) -> List[Tuple[Any, str]]: return PygmentsTokens(self._status_bar.get_tokens()) async def parse_and_evaluate(self, input): command_parts = split_command(input) if command_parts and command_parts[0]: cmd = command_parts[0] args = command_parts[1] if len(command_parts) > 1 else None return await self.evaluate_command(cmd, args, input) async def evaluate_command(self, cmd, args, raw): args = args or "" if cmd in self._command_registry: cmd_instance = self._command_registry.find_command(cmd) else: suggestions = find_approx( cmd, self._command_registry.get_all_commands_map() ) if self._options.auto_execute_single_suggestions and len(suggestions) == 1: print() cprint( "Auto-correcting '{}' to '{}'".format(cmd, suggestions[0]), "red", attrs=["bold"], ) cmd_instance = self._command_registry.find_command(suggestions[0]) else: print() cprint( "Unknown Command '{}'{} type `help` to see all " "available commands".format(cmd, suggestions_msg(suggestions)), "red", attrs=["bold"], ) cmd_instance = None if cmd_instance is not None: try: ret = self._blacklist.is_blacklisted(cmd) if ret: return ret except Exception as e: err_message = ( "Blacklist executing failed, " "all commands are available.\n" "{}".format(str(e)) ) cprint(err_message, "red") logging.error(err_message) try: catchall(self._usagelogger.pre_exec) result = await try_await(cmd_instance.run_interactive(cmd, args, raw)) catchall(self._usagelogger.post_exec, cmd, args, result, False) self._status_bar.set_last_command_status(result) return result except NotImplementedError as e: cprint("[NOT IMPLEMENTED]: {}".format(str(e)), "yellow", attrs=["bold"]) # not implemented error code return 99 async def run(self): prompt = self._build_cli() self._status_bar.start() try: while True: try: with patch_stdout(): text = await prompt.prompt_async( PygmentsTokens(self._get_prompt_tokens()), rprompt=PygmentsTokens( self._status_bar.get_rprompt_tokens() ), ) session_logger = self._plugin.get_session_logger(self._ctx) if session_logger: # Commands don't get written to stdout, so we have to # explicitly dump them to the session log. session_logger.log_command(text) with session_logger.patch(): await self.parse_and_evaluate(text) else: await self.parse_and_evaluate(text) except KeyboardInterrupt: pass except EOFError: # Application exiting. pass self._status_bar.stop() async def on_connected(self, *args, **kwargs): pass class ShellCompleter(Completer): def __init__(self, command_registry): super(Completer, self).__init__() self._command_registry = command_registry def get_completions(self, document, complete_event): if document.on_first_line: cmd_and_args = split_command(document.text_before_cursor) # are we the first word? suggest from command names if len(cmd_and_args) > 1: cmd, args = cmd_and_args # pass to the children # let's find the parent command first cmd_instance = self._command_registry.find_command(cmd) if not cmd_instance: return [] return cmd_instance.get_completions( cmd, Document( args, document.cursor_position - len(document.text) + len(args) ), complete_event, ) else: return self._command_registry.get_completions(document, complete_event) return [] python-nubia-0.2.3/nubia/internal/io/000077500000000000000000000000001434345131700174575ustar00rootroot00000000000000python-nubia-0.2.3/nubia/internal/io/__init__.py000066400000000000000000000003501434345131700215660ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/nubia/internal/io/eventbus.py000066400000000000000000000016271434345131700216720ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import logging import traceback from nubia.internal.helpers import try_await logger = logging.getLogger(__name__) class Message: CONNECTED = 1 class Listener: async def react(self, msg, *args, **kwargs): if msg == Message.CONNECTED: try: await try_await(self.on_connected(*args, **kwargs)) except NotImplementedError: raise except Exception as e: logger.info("Couldn't initialize {}: " "{}".format(type(self), e)) traceback.print_exc() async def on_connected(*args, **kwargs): raise NotImplementedError("Listeners must implement on_connected method") python-nubia-0.2.3/nubia/internal/io/logger.py000066400000000000000000000031211434345131700213050ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import logging import threading from termcolor import colored class ContextFilter(logging.Filter): def filter(self, record): # colorize the level level = record.levelname.lower().rjust(7) if record.levelno <= logging.DEBUG: level = colored(level, "blue") elif record.levelno >= logging.ERROR: level = colored(level, "red") elif record.levelno >= logging.WARNING: level = colored(level, "yellow") record.level = level # logger name if record.name == "__main__": logger_name = "main" else: logger_name = record.name.split(".")[-1] record.logger_name = logger_name # thread name (optional) record.thread = "" if record.levelno <= logging.DEBUG: thread = threading.current_thread().getName() if thread != "MainThread": record.thread = "thread {}: ".format(thread) return True def get_formatter(): return logging.Formatter( fmt="[%(asctime)-15s] [%(level)6s] [%(logger_name)s] %(thread)s%(message)s" ) def setup_logger(level, stream): log_handler = logging.StreamHandler(stream) log_handler.setFormatter(get_formatter()) log_handler.addFilter(ContextFilter()) logging.root.addHandler(log_handler) logging.root.setLevel(level) python-nubia-0.2.3/nubia/internal/io/session_logger.py000066400000000000000000000033321434345131700230540ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import re import sys import threading from contextlib import contextmanager class SessionLogger: """ SessionLogger is used to intercept stdout content and duplicates it to a session log file. Inspired from prompt_toolkit.StdoutProxy but without the buffering. """ def __init__(self, file): self._log_file = file self._lock = threading.RLock() self.original_stdout = sys.stdout # errors/encoding attribute for compatibility with sys.stdout. self.errors = sys.stdout.errors self.encoding = sys.stdout.encoding def path(self): return self._log_file.name def log_command(self, cmd): cmd = self._strip_ansii_colors(cmd) with self._lock: self._log_file.write(f"\n> {cmd}\n") def write(self, data): with self._lock: self.original_stdout.write(data) self._log_file.write(self._strip_ansii_colors(data)) def flush(self): with self._lock: self.original_stdout.flush() self._log_file.flush() def _strip_ansii_colors(self, text): return re.sub("\x1b\\[.+?m", "", text) def isatty(self): return self.original_stdout.isatty() def fileno(self): return self.original_stdout.fileno() @contextmanager def patch(self): original_stdout = sys.stdout sys.stdout = self try: yield finally: self.flush() sys.stdout = original_stdout python-nubia-0.2.3/nubia/internal/ipython.py000066400000000000000000000035161434345131700211210ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import sys from traitlets.config.loader import Config from nubia.internal.helpers import try_await from nubia.internal.ui.ipython import NubiaPrompt try: from IPython.terminal.embed import InteractiveShellEmbed except ImportError: raise Exception("IPython is not installed, cannot use IPython-based shell") custom_locals = {} async def start_interactive_python(plugin, registry, ctx, args): await try_await(ctx.on_interactive(args)) cmds = list(registry.get_all_commands()) for cmd in cmds: # TODO: This currently works for AutoCommands only, it's a hack to get # the command as a function, clean this up and make # _get_executable_function a public member. if hasattr(cmd, "_get_executable_function"): executable = cmd._get_executable_function() names = cmd.get_command_names() for name in names: # function names cannot have - in them name = name.replace("-", "_") custom_locals[name] = executable cfg = Config() cfg.TerminalInteractiveShell.prompts_class = NubiaPrompt # Custom Config cfg.InteractiveShellEmbed.autocall = 2 cfg.InteractiveShellEmbed.autoawait = True banner = "LogDevice IPython Shell; Python {}".format(sys.version) ipkwargs = { "config": cfg, "banner1": banner, "banner2": "\n", "user_ns": custom_locals, } plugin.update_ipython_kwargs(ctx=ctx, kwargs=ipkwargs) ipshell = InteractiveShellEmbed(**ipkwargs) for magic in plugin.get_magics(): ipshell.register_magics(magic) ipshell() python-nubia-0.2.3/nubia/internal/nubia.py000066400000000000000000000275221434345131700205300ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse import asyncio import codecs import locale import logging import os import sys import tempfile import traceback import typing from nubia.internal import cmdloader, context, exceptions from nubia.internal.blackcmd import CommandBlacklist from nubia.internal.cmdbase import AutoCommand from nubia.internal.commands import builtin, help from nubia.internal.helpers import catchall, try_await from nubia.internal.interactive import IOLoop from nubia.internal.io import logger from nubia.internal.options import Options from nubia.internal.plugin_interface import PluginInterface from nubia.internal.registry import CommandsRegistry from nubia.internal.typing.argparse import create_subparser_class from nubia.internal.usage_logger_interface import UsageLoggerInterface from termcolor import cprint def set_default_subparser(self, name, args=None): """default subparser selection. Call after setup, just before parse_args() name: is the name of the subparser to call by default args: if set is the argument list handed to parse_args() tested with 2.7, 3.2, 3.3, 3.4 it works with 2.6 assuming argparse is installed """ subparser_found = False for arg in sys.argv[1:]: if arg in ["-h", "--help"]: # global help if no subparser break else: for x in self._subparsers._actions: if not isinstance(x, argparse._SubParsersAction): continue for sp_name in x._name_parser_map.keys(): if sp_name in sys.argv[1:]: subparser_found = True if not subparser_found: # insert default in the last position, this implies no # global options without a sub_parsers specified if args is None: sys.argv.append(name) else: args.append(name) # Injects this method in ArgumentParser, this is to support default subparsers # in Python < 3 while it also works with python > 3 argparse.ArgumentParser.set_default_subparser = set_default_subparser class Nubia: """ This is the core class that creates and runs nubia, the constructor takes a number of arguments that control how nubia should behave and which plugin it will load on startup. """ def __init__( self, name, command_pkgs=None, plugin: typing.Optional[PluginInterface] = None, testing: bool = False, options: typing.Optional[Options] = None, ): self._name = name self._plugin = plugin or PluginInterface() self._options = options or Options() assert isinstance(self._plugin, PluginInterface) self._blacklist = self._plugin.getBlacklistPlugin() self._command_pkgs = command_pkgs if self._blacklist is not None: assert isinstance(self._blacklist, CommandBlacklist) self._testing = testing # Setting the context to be global context._ctx = self._plugin.create_context() self._ctx = context.get_context() assert isinstance(self._ctx, context.Context) # Setting the binary name self._ctx.set_binary_name(self._name) # Load, setup the usagelogger self._usagelogger = None def _setup_logging(self, args): root_logger = self._plugin.setup_logging(logging.root, args) if root_logger: return if args.verbose and args.verbose >= 2: logging_level = logging.DEBUG elif args.verbose == 1: logging_level = logging.INFO else: logging_level = logging.WARN if args.stderr: logging_stream = sys.stderr else: logging_stream = tempfile.NamedTemporaryFile( mode="w+", # default is 'w+b', oddly enough prefix="{}-".format(self._name), delete=False, ) print("Logging to {}".format(logging_stream.name), file=sys.stderr) logger.setup_logger(level=logging_level, stream=logging_stream) async def start_ipython(self, args): from nubia.internal.ipython import start_interactive_python return await start_interactive_python( self._plugin, self._registry, self._ctx, args ) @property def usage_logger(self): if not self._usagelogger: self._usagelogger = self._plugin.create_usage_logger( self._ctx ) or UsageLoggerInterface(self._ctx) assert isinstance(self._usagelogger, UsageLoggerInterface) return self._usagelogger def _setup_terminal(self, args): # Setup the codec for writing unicode to stdout. This also correctly # encodes unicode if the standard output is ascii and prevents python # from crashing with UnicodeEncodingError # Only required for Python 2 if sys.version_info[0] == 2: writer = codecs.getwriter(locale.getpreferredencoding()) sys.stdout = writer(sys.stdout) if getattr(args, "no_color", False) or not sys.stdout.isatty(): os.environ["ANSI_COLORS_DISABLED"] = "True" async def _create_interactive_io_loop(self, args): io_loop = IOLoop(self._ctx, self._plugin, self.usage_logger, self._options) await try_await(self._ctx.on_interactive(args)) return io_loop async def start_interactive(self, args): io_loop = await self._create_interactive_io_loop(args) ret = 0 # Only run the Interactive mode if std is a tty, otherwise # we should rad the input from stdin, process it, and exit. if sys.stdin.isatty(): await io_loop.run() return ret else: # Read the command from stdin and run commands = sys.stdin.readlines() for command in commands: # execute print("> {}".format(command)) ret = await io_loop.parse_and_evaluate(command) # We fail execution on the first failing command if ret: return ret return ret def _parse_args(self, cli_args=sys.argv): cli_args = cli_args[1:] # remove binary name args, extra = self._opts_parser.parse_known_args(args=cli_args) # this allows subcommand specific args to be inserted anywhere in the # cli, for instance: # my_prog --atonce=5 -vv -t status --stderr # It essentially puts all unrecognized args in the end of the cli # invocation and try parsing again if extra: for extra_arg in extra: cli_args.remove(extra_arg) cli_args.append(extra_arg) args = self._opts_parser.parse_args(args=cli_args) return args def _validate_args(self, args): try: # The argument validation will raise ArgsValidationError self._plugin.validate_args(args) except exceptions.ArgsValidationError as e: cprint("Arguments validation error: {}".format(str(e)), "red") return 1 except Exception as e: cprint( "An exception occurred while validating the command " "arguments: {}".format(str(e)), "red", ) return 1 async def run_cli(self, args): catchall(self.usage_logger.pre_exec) try: ret = self._blacklist.is_blacklisted(args._cmd) if ret: return ret except Exception as e: err_message = ( "Blacklist executing failed, " "all commands are available.\n" "{}".format(str(e)) ) cprint(err_message, "red") logging.error(err_message) await try_await(self._ctx.on_cli(args._cmd, args)) ret = await try_await(self._registry.find_command(args._cmd).run_cli(args)) return ret async def _pre_run(self, cli_args): self._opts_parser = self._plugin.get_opts_parser() SubParser = create_subparser_class(self._opts_parser) self._opts_parser.add_argument( "--_print-completion-model", action="store_true", help=argparse.SUPPRESS ) cmd_parser = self._opts_parser.add_subparsers( dest="_cmd", help="Subcommand to run, if missing the interactive mode is started" " instead.", parser_class=SubParser, metavar="[command]", ) builtin_cmds = [ builtin.Connect, builtin.Exit, builtin.Verbose, help.HelpCommand, ] listeners = self._plugin.get_listeners() self._registry = CommandsRegistry(cmd_parser, listeners) self._ctx.set_registry(self._registry) self._registry.register_priority_listener(self._ctx) # register built-in commands for cmd in builtin_cmds: await self._registry.register_command(cmd()) # load commands from plugin for cmd in self._plugin.get_commands(): await self._registry.register_command(cmd, override=True) # load commands from command packages if not isinstance(self._command_pkgs, list): self._command_pkgs = [self._command_pkgs] for pkg in self._command_pkgs: for cmd in cmdloader.load_commands(pkg): await self._registry.register_command( AutoCommand(cmd, self._options), override=True ) # By default, if we didn't receive any command we will use the connect # command which drops us to an interactive mode. self._opts_parser.set_default_subparser("connect") args = self._parse_args(cli_args) self._setup_logging(args) # check if we can add colors to stdout self._setup_terminal(args) self._validate_args(args) self._ctx.set_args(args) self._registry.set_cli_args(args) return args def run(self, cli_args=sys.argv, ipython=False): if sys.version_info[0] == 3 and sys.version_info[1] >= 7: return asyncio.run(self.run_async(cli_args, ipython)) loop = asyncio.get_event_loop() return loop.run_until_complete(self.run_async(cli_args, ipython)) async def run_async(self, cli_args=sys.argv, ipython=False): """ Runs nubia either in interactive or cli (or parsing commands from stdin) based on the cli_args supplied (defaults to sys.argv). This will block until the shell is done processing all the input and will return the exit code. """ args = await self._pre_run(cli_args) if args._print_completion_model: from nubia.internal import registry_tools as regtools try: data = regtools.export_registry( self._plugin, args, self._opts_parser, self._registry ) print(data) return 0 except Exception as e: print("Failed to export model: {}".format(e), file=sys.stderr) traceback.print_exc() return 1 if ipython: return await self.start_ipython(args) # by default, if no command is passed we will get 'connect' if args._cmd == "connect": return await self.start_interactive(args) else: ret = await self.run_cli(args) catchall(self.usage_logger.post_exec, args._cmd, cli_args, ret, True) if type(ret) is int: return ret if type(ret) is bool: return int(not (ret)) if ret is None: return 0 else: return 1 python-nubia-0.2.3/nubia/internal/options.py000066400000000000000000000025021434345131700211140ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from dataclasses import dataclass @dataclass class Options: """Class for defining Nubia options and settings""" # File-based history is enabled by default. If this is set to false, we # fallback to the in-memory history. persistent_history: bool = True # Auto-executing single suggestions is enabled by default. # - if there is a single prefix suggestion (unique prefix match) it automatically # executes it # - if there are multiple prefix suggestions, it prints a message with the # suggestions # - if there are no prefix suggestions, and there's a single levenshtein # suggestion it automatically executes it # - if there are multiple levenshtein suggestions, it prints a message with the # suggestions # If this is set to false it just prints the suggestions auto_execute_single_suggestions: bool = True # Limit the choices displayed to this number. All choices are available, just # what is displayed is limited to this number to avoid having the user scroll down # a long list. limit_visible_choices: int = 10 python-nubia-0.2.3/nubia/internal/parser.py000066400000000000000000000070311434345131700207170ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import pyparsing as pp from nubia.internal.exceptions import CommandParseError allowed_symbols_in_string = r"-_/#@£$€%*+~|<>?." def _no_transform(x): return x def _bool_transform(x): return x in ["True", "true"] def _str_transform(x): if x and ((x[0] == x[-1] == '"') or (x[0] == x[-1] == "'")): return x[1:-1] return x _TRANSFORMS = { "bool": _bool_transform, "str": _str_transform, "int": int, "float": float, "dict": dict, } def _parse_type(datatype): transform = _TRANSFORMS.get(datatype, _no_transform) def _parse(s, loc, toks): return list(map(transform, toks)) return _parse identifier = pp.Word(pp.alphas + "_-", pp.alphanums + "_-") int_value = pp.Regex(r"\-?\d+").setParseAction(_parse_type("int")) float_value = pp.Regex(r"\-?\d+\.\d*([eE]\d+)?").setParseAction(_parse_type("float")) bool_value = ( pp.Literal("True") ^ pp.Literal("true") ^ pp.Literal("False") ^ pp.Literal("false") ).setParseAction(_parse_type("bool")) # may have spaces quoted_string = pp.quotedString.setParseAction(_parse_type("str")) # cannot have spaces unquoted_string = pp.Word(pp.alphanums + allowed_symbols_in_string).setParseAction( _parse_type("str") ) string_value = quoted_string | unquoted_string single_value = bool_value | string_value | float_value | int_value list_value = pp.Group( pp.Suppress("[") + pp.Optional(pp.delimitedList(single_value)) + pp.Suppress("]") ).setParseAction(_parse_type("list")) # because this is a recursive construct, a dict can contain dicts in values dict_value = pp.Forward() value = list_value ^ single_value ^ dict_value dict_key_value = pp.dictOf(string_value + pp.Suppress(":"), value) dict_value << pp.Group( pp.Suppress("{") + pp.delimitedList(dict_key_value) + pp.Suppress("}") ).setParseAction(_parse_type("dict")) # Positionals must be end of line or has a space (or more) afterwards. # This is to ensure that the parser treats text like "something=" as invalid # instead of parsing this as positional "something" and leaving the "=" as # invalid on its own. positionals = pp.ZeroOrMore( value + (pp.StringEnd() ^ pp.Suppress(pp.OneOrMore(pp.White()))) ).setResultsName("positionals") key_value = pp.Dict( pp.ZeroOrMore(pp.Group(identifier + pp.Suppress("=") + value)) ).setResultsName("kv") subcommand = identifier.setResultsName("__subcommand__") # Subcommand is optional here as it maybe missing, in this case we still want to # pass the parsing and we will handle the fact that the subcommand is missing # while validating the arguments command_with_subcommand = pp.Optional(subcommand) + key_value + positionals # Positionals will be passed as the last argument command = key_value + positionals def parse(text: str, expect_subcommand: bool) -> pp.ParseResults: expected_pattern = command_with_subcommand if expect_subcommand else command try: result = expected_pattern.parseString(text, parseAll=True) return result except pp.ParseException as e: exception = CommandParseError(str(e)) remaining = e.markInputline() partial_result = expected_pattern.parseString(text, parseAll=False) exception.remaining = remaining[(remaining.find(">!<") + 3) :] exception.partial_result = partial_result exception.col = e.col raise exception python-nubia-0.2.3/nubia/internal/plugin_interface.py000066400000000000000000000117341434345131700227460ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse from typing import Any, List, MutableMapping, Tuple from nubia.internal.blackcmd import CommandBlacklist from nubia.internal.constants import DEFAULT_COMMAND_TIMEOUT from nubia.internal.context import Context from nubia.internal.ui import statusbar class CompletionDataSource: """An interface that defines completion data sources""" def get_all(self): """ Returns all the possible values for this data source """ return [] class PluginInterface: """ The PluginInterface class is a way to customize nubia for every customer use case. It allowes custom argument validation, control over command loading, custom context objects, and much more. """ def create_context(self): """ Must create an object that inherits from `Context` parent class. The plugin can return a custom context but it has to inherit from the correct parent class. """ return Context() def validate_args(self, args): """ This will be executed when starting nubia, the args passed is a dict-like object that contains the argparse result after parsing the command line arguments. The plugin can choose to update the context with the values, and/or decide to raise `ArgsValidationError` with the error message. """ pass def get_commands(self): return [] def get_listeners(self): """ Return all "classes" that implement the Listener interface, note that you should not return the instances of these classes as they will be instantiated by nubia """ return [] def get_magics(self): """ Return all the class objects that inherit from `IPython.core.magic.Magics` to be registered if running with ipython mode. """ return [] def get_opts_parser(self, add_help=True): """ Builds the ArgumentParser that will be passed to nubia, use this to build your list of arguments that you want for your shell. """ epilog = ( "NOTES: LIST types are given as comma separated values, " "eg. a,b,c,d. DICT types are given as semicolon separated " "key:value pairs (or key=value), e.g., a:b;c:d and if a dict " "takes a list as value it look like a:1,2;b:1" ) opts_parser = argparse.ArgumentParser( description="A Generic Shell Utility", epilog=epilog, formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=add_help, ) opts_parser.add_argument( "--verbose", "-v", action="count", default=0, help="Increase verbosity, can be specified multiple times", ) opts_parser.add_argument( "--stderr", "-s", action="store_true", help="By default the logging output goes to a " "temporary file. This disables this feature " "by sending the logging output to stderr", ) opts_parser.add_argument( "--command-timeout", required=False, type=int, default=DEFAULT_COMMAND_TIMEOUT, help="Timeout for commands (default %ds)" % DEFAULT_COMMAND_TIMEOUT, ) return opts_parser def get_completion_datasource_for_global_argument(self, name): return None def get_status_bar(self, context): return statusbar.StatusBar(context) def get_prompt_tokens(self, context: Context) -> List[Tuple[Any, str]]: return context.get_prompt_tokens() def setup_logging(self, root_logger, args): """ Override this and configure your own logging setup. Return your root logger. """ return None def create_usage_logger(self, context): """ Override this and return you own usage logger. Must be a subtype of UsageLoggerInterface. """ return None def getBlacklistPlugin(self): """ Override this and return you own plugin for blacklist commands. Then implement a function is_blacklisted() Any return other then 0 will block command execution """ return CommandBlacklist() def update_ipython_kwargs( self, ctx: Context, kwargs: MutableMapping[str, Any] ) -> None: """ Return named arguments that need to be added when calling InteractiveShellEmbed. """ pass def get_session_logger(self, context): """ Override this and return your own instance of SessionLogger if you want to enable session logging. """ return None python-nubia-0.2.3/nubia/internal/registry.py000066400000000000000000000101421434345131700212700ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from prompt_toolkit.completion import WordCompleter from termcolor import cprint from nubia.internal.cmdbase import Command from nubia.internal.helpers import try_await from nubia.internal.io.eventbus import Listener class CommandsRegistry: """ A registry that holds all commands implementations and creates a quick access point for resolving a command string into the corresponding handling object """ def __init__(self, parser, listeners): self._completer = WordCompleter([], ignore_case=True, sentence=True) # maps a command to Command Instance self._cmd_instance_map = {} # objects interested in receiving messages self._listeners = [] # argparser so each command can add its options self._parser = parser for lst in listeners: self.register_listener(lst(self)) async def register_command(self, cmd_instance, override=False): if not isinstance(cmd_instance, Command): raise TypeError( "Invalid command instance, must be an instance of " "subclass of Command" ) cmd_instance.set_command_registry(self) cmd_keys = cmd_instance.get_command_names() for cmd in cmd_keys: if not cmd_instance.get_help(cmd): cprint( ( "[WARNING] The command {} will not be loaded. " "Please provide a help message by either defining a " "docstring or filling the help argument in the " "@command annotation" ).format(cmd_keys[0]), "red", ) return None await try_await(cmd_instance.add_arguments(self._parser)) if not override: conflicts = [cmd for cmd in cmd_keys if cmd in self._cmd_instance_map] if conflicts: raise ValueError( "Some other command instance has registered " "the name(s) {}".format(conflicts) ) if isinstance(cmd_instance, Listener): self._listeners.append(cmd_instance) for cmd in cmd_keys: self._cmd_instance_map[cmd.lower()] = cmd_instance if cmd not in self._completer.words: self._completer.words.append(cmd) self._completer.meta_dict[cmd] = cmd_instance.get_help_short(cmd) aliases = cmd_instance.get_cli_aliases() for alias in aliases: self._cmd_instance_map[alias.lower()] = cmd_instance def register_priority_listener(self, instance): """ Registers a listener that get the top priority in callbacks """ if not isinstance(instance, Listener): raise TypeError("Only Listeners can be registered") self._listeners.insert(0, instance) def register_listener(self, instance): if not isinstance(instance, Listener): raise TypeError("Only Listeners can be registered") self._listeners.append(instance) def __contains__(self, cmd): return cmd.lower() in self._cmd_instance_map def get_completer(self): return self._completer def get_all_commands(self): return set(self._cmd_instance_map.values()) def get_all_commands_map(self): return self._cmd_instance_map def find_command(self, cmd): return self._cmd_instance_map.get(cmd.lower()) def get_completions(self, document, complete_event): return self._completer.get_completions(document, complete_event) async def dispatch_message(self, msg, *args, **kwargs): for mod in self._listeners: await try_await(mod.react(msg, *args, **kwargs)) def set_cli_args(self, args): self._args = args def get_cli_arg(self, arg): return getattr(self._args, arg, None) python-nubia-0.2.3/nubia/internal/registry_tools.py000066400000000000000000000077021434345131700225200ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from typing import Callable import json import logging from argparse import _SubParsersAction from nubia.internal.typing import Command, FunctionInspection from nubia.internal.typing.argparse import transform_argument_name logger = logging.getLogger(__name__) def _dump_command(cmd): assert isinstance(cmd, Command) return { # We can also add cmd.help if needed in the future "name": cmd.name } def _dump_arguments(arguments): output = {"options": [], "positionals": []} for arg in arguments.values(): if arg.positional: output["positionals"].append( { "name": transform_argument_name(arg.name), "values": list(arg.choices) if arg.choices and not isinstance(arg.choices, Callable) else arg.choices, } ) else: output["options"].append( { "name": transform_argument_name(arg.name), "extra_names": list(map(transform_argument_name, arg.extra_names)), "expects_argument": not ( arg.type == bool or arg.default_value is False ), "default": arg.default_value, "required": not arg.default_value_set, "values": list(arg.choices) if arg.choices and not isinstance(arg.choices, Callable) else arg.choices, } ) return output def _dump_subcommands(subcommands): return [_fn_to_dict(cmd) for _, cmd in subcommands] def _dump_opts_parser_common(opts_parser, plugin): output = [] top_level_actions = [ action for action in opts_parser._actions if not isinstance(action, _SubParsersAction) ] for action in top_level_actions: option = {"extra_names": []} for name in action.option_strings: if name.startswith(opts_parser.prefix_chars * 2): option["name"] = name elif name.startswith(opts_parser.prefix_chars): option["extra_names"].append(name) else: # we don't know what that is! logger.warning( "We found '%s' in option_strings of action %s", name, action ) # we want to skip this particular one since it's hidden if option.get("name", "").startswith("--_"): continue option["expects_argument"] = True if action.type is not None else False option_name = option.get("name") if option_name: ds = plugin.get_completion_datasource_for_global_argument(option_name) if ds: option["values"] = ds.get_all() output.append(option) return output def _fn_to_dict(inspection): cmd = _dump_command(inspection.command) cmd.update(_dump_arguments(inspection.arguments)) if inspection.subcommands: cmd["commands"] = _dump_subcommands(inspection.subcommands) return cmd def export_registry(plugin, args, opts_parser, registry): cmds = registry.get_all_commands() commands = [] for cmd in cmds: if cmd.built_in: continue inspection = cmd.metadata if isinstance(inspection, FunctionInspection): commands.append(_fn_to_dict(inspection)) else: logger.warning("Command %s is not instance of FunctionInspection", cmd) model = { "commands": commands, # This will include the shell top-level options, this will be included # in a future diff "options": _dump_opts_parser_common(opts_parser, plugin), } return json.dumps(model) python-nubia-0.2.3/nubia/internal/typing/000077500000000000000000000000001434345131700203625ustar00rootroot00000000000000python-nubia-0.2.3/nubia/internal/typing/__init__.py000066400000000000000000000332541434345131700225020ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # """ Module for giving type support for nubia Check argparse.py for functions to work along with argparse in the cli mode Check builder.py for functions to work along with nubia in interactive mode This module contains 3 important functions: - command - argument - inspect_object `command` and `argument` are function/method decorators. It works as an annotation and is meant to be used only on top of the function/method declaration. They are used together to indicate that a particular function or method should be exported for usage in nubia For example, the following `foo` function can be exported as the `execute` command: @command('execute', help='This command executes something') @argument('arg1', type=str, description='arg1 must be a string', aliases=['a']) @argument('arg2', type=typing.List[int], description='arg2 must be a list of integers' aliases=['b']) def foo(arg1, arg2): ... And then it can be called in the cli as % my_prog -t execute -a something -b 1,2,3 or in nubia interactive mode as execute a=something b=[1,2,3] `foo` will be called already with the correct types as well Use `inspect_object` to analyze and retrieve the added metadata of an annotated function/method Check tests.py present in this module for more usage examples Notes: The @argument decorator is compatible with python 3 typing annotations. The following example achieves the exact same result as the example above: @command('execute', help='This command executes something') @argument('arg1', description='arg1 must be a string', aliases=['a']) @argument('arg2', description='arg2 must be a list of integers' aliases=['b']) def foo(arg1: str, arg2: typing.List[int]): ... """ from collections import OrderedDict, namedtuple from collections.abc import Container from functools import partial from inspect import isclass, ismethod from termcolor import cprint from nubia.internal.helpers import ( function_to_str, get_arg_spec, transform_class_name, transform_name, ) Argument = namedtuple( "Argument", "arg description type " "default_value_set default_value " "name extra_names positional choices", ) Command = namedtuple("Command", "name help aliases exclusive_arguments") FunctionInspection = namedtuple("FunctionInspection", "arguments command subcommands") _ArgDecoratorSpec = namedtuple( "_ArgDecoratorSpec", "arg name aliases description positional choices" ) def _empty_arg_decorator_spec(arg): return _ArgDecoratorSpec( arg=arg, name=transform_name(arg), aliases=[], description=None, positional=False, choices=None, ) def append_doc(func, arg, type, description): func.__doc__ = "%s\r\n\r\n%s\t%s\t%s" % ( func.__doc__, arg.ljust(30, " "), type, description.replace("\n", ""), ) return func def argument( arg, type=None, description=None, name=None, aliases=None, positional=False, choices=None, ): """ Annotation decorator to specify metadata for an argument Check the module documentation for more info and tests.py in this module for usage examples """ def decorator(function): # Following makes interactive really slow. (T20898480) # This should be revisited in T20899641 # if (description is not None and \ # arg is not None and type is not None): # append_doc(function, arg, type, description) fn_specs = get_arg_spec(function) args = fn_specs.args or [] if arg not in args and not fn_specs.varkw: raise NameError( "Argument {} does not exist in function {}".format( arg, function_to_str(function) ) ) # init structures to store decorator data if not present _init_attr(function, "__annotations__", OrderedDict()) _init_attr(function, "__arguments_decorator_specs", {}) # Check if there is a conflict in type annotations current_type = function.__annotations__.get(arg) if current_type and type and current_type != type: raise TypeError( "Argument {} in {} is both specified as {} " "and {}".format(arg, function_to_str(function), current_type, type) ) if arg in function.__arguments_decorator_specs: raise ValueError( "@argument decorator was applied twice " "for the same argument {} on function {}".format(arg, function) ) if positional and aliases: msg = "Aliases are not yet supported for positional arguments @ {}".format( arg ) raise ValueError(msg) # reject positional=True if we are applied over a class if isclass(function) and positional: raise ValueError("Cannot set positional arguments for super commands") # We use __annotations__ to allow the usage of python 3 typing function.__annotations__.setdefault(arg, type) function.__arguments_decorator_specs[arg] = _ArgDecoratorSpec( arg=arg, description=description, name=name or transform_name(arg), aliases=aliases or [], positional=positional, choices=choices or [], ) return function return decorator def command(name_or_function=None, help=None, aliases=None, exclusive_arguments=None): """ Annotation decorator to specify that a function or method is a command that should be exported by nubia Check the module documentation for more info and tests.py in this module for usage examples """ def decorator(function, name=None): is_supercommand = isclass(name_or_function) exclusive_arguments_ = _normalize_exclusive_arguments(exclusive_arguments) _validate_exclusive_arguments(function, exclusive_arguments_) _init_attr(function, "__command", {}) if name: function.__command["name"] = name else: function.__command["name"] = ( transform_name(function.__name__) if not is_supercommand else transform_class_name(function.__name__) ) function.__command["help"] = help function.__command["aliases"] = aliases or [] function.__command["exclusive_arguments"] = exclusive_arguments_ return function # Allows the decorator to be used directly (`@command`) or as a # function call (`@command()`) if callable(name_or_function): function = name_or_function return decorator(function) else: name = name_or_function return partial(decorator, name=name) def inspect_object(obj, accept_bound_methods=False): """ Used to inspect a function or method annotated with @command or @argument. Returns a well structured dict summarizing the metadata added through the decorators Check the module documentation for more info """ command = getattr(obj, "__command", None) arguments_decorator_specs = getattr(obj, "__arguments_decorator_specs", {}) argspec = get_arg_spec(obj) args = argspec.args # remove the first argument in case this is a method (normally the first # arg is 'self') if ismethod(obj): args = args[1:] result = {"arguments": OrderedDict(), "command": None, "subcommands": {}} if command: result["command"] = Command( name=command["name"] or obj.__name__, help=command["help"] or obj.__doc__, aliases=command["aliases"], exclusive_arguments=command["exclusive_arguments"], ) # Is this a super command? is_supercommand = isclass(obj) for i, arg in enumerate(args): if (is_supercommand or accept_bound_methods) and arg == "self": continue arg_idx_with_default = len(args) - len(argspec.defaults) default_value_set = bool(argspec.defaults and i >= arg_idx_with_default) default_value = ( argspec.defaults[i - arg_idx_with_default] if default_value_set else None ) # We will reject classes (super-commands) that has required arguments to # reduce complexity if is_supercommand and not default_value_set: raise ValueError( "Cannot accept super commands that has required " "arguments with no default value " "like '{}' in super-command '{}'".format(arg, result["command"].name) ) arg_decor_spec = arguments_decorator_specs.get( arg, _empty_arg_decorator_spec(arg) ) result["arguments"][arg_decor_spec.name] = Argument( arg=arg_decor_spec.arg, description=arg_decor_spec.description, type=argspec.annotations.get(arg), default_value_set=default_value_set, default_value=default_value, name=arg_decor_spec.name, extra_names=arg_decor_spec.aliases, positional=arg_decor_spec.positional, choices=arg_decor_spec.choices, ) if argspec.varkw: # We will inject all the arguments that are not defined explicitly in # the function signature. for arg, arg_decor_spec in arguments_decorator_specs.items(): added_arguments = [v.name for v in result["arguments"].values()] if arg_decor_spec.name not in added_arguments: # This is an extra argument result["arguments"][arg_decor_spec.name] = Argument( arg=arg, description=arg_decor_spec.description, type=argspec.annotations.get(arg), default_value_set=True, default_value=None, name=arg_decor_spec.name, extra_names=arg_decor_spec.aliases, positional=arg_decor_spec.positional, choices=arg_decor_spec.choices, ) # Super Command Support if is_supercommand: result["subcommands"] = [] for attr in dir(obj): if attr.startswith("_"): # ignore "private" methods continue candidate = getattr(obj, attr) if not callable(candidate): # avoid e.g. properties continue metadata = inspect_object(candidate, accept_bound_methods=True) # ignore subcommands without docstring if metadata.command: if not metadata.command.help: cprint( ( f"[WARNING] The sub-command {metadata.command.name} " "will not be loaded. " "Please provide a help message by either defining a " "docstring or filling the help argument in the " "@command annotation" ), "red", ) continue result["subcommands"].append((attr, metadata)) return FunctionInspection(**result) def _init_attr(obj, attribute, default_value): if not hasattr(obj, attribute): setattr(obj, attribute, default_value) def _normalize_exclusive_arguments(exclusive_arguments): """ Guarantees that exclusive arguments is normalized to a tuple of tuples or None in case exclusive_arguments is empty """ def all_string_items(items): return all(isinstance(item, str) for item in items) def all_container_items(items): return all( isinstance(item, Container) and not isinstance(item, str) for item in items ) if not exclusive_arguments: return None if all_string_items(exclusive_arguments): return (tuple(exclusive_arguments),) is_container_of_container_of_strings = all_container_items( exclusive_arguments ) and all(all_string_items(item) for item in exclusive_arguments) if not is_container_of_container_of_strings: raise ValueError( "exclusive_arguments is not an array of " "strings or an array of arrays of strings" ) return tuple(tuple(group) for group in exclusive_arguments) def _validate_exclusive_arguments(function, normalized_exclusive_arguments): if not normalized_exclusive_arguments: return exclusive_arguments = normalized_exclusive_arguments flat_ex_args = [arg for group in exclusive_arguments for arg in group] if not flat_ex_args: return inspection = inspect_object(function) possible_args = list(inspection.arguments.keys()) unknown_args = set(flat_ex_args) - set(possible_args) if unknown_args: msg = ( "The following arguments were specified as exclusive but they " "are not present in function {}: {}".format( function_to_str(function), ", ".join(unknown_args) ) ) raise NameError(msg) if len(set(flat_ex_args)) != len(flat_ex_args): counts = ( (item, group.count(item)) for group in exclusive_arguments for item in group ) repeated_args = [item for item, count in counts if count > 1] msg = ( "The following args are present in more than one exclusive " "group: {}".format(", ".join(repeated_args)) ) raise ValueError(msg) python-nubia-0.2.3/nubia/internal/typing/argparse.py000066400000000000000000000303401434345131700225400ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse import copy import os import shutil import subprocess import sys from collections import defaultdict from functools import partial from typing import Any, Dict, List, Tuple, Callable from nubia.internal.helpers import try_await # noqa F401 from nubia.internal.typing.builder import ( build_value, get_dict_kv_arg_type_as_str, get_list_arg_type_as_str, ) from nubia.internal.typing.inspect import ( get_first_type_argument, is_iterable_type, is_mapping_type, is_optional_type, ) from . import command, inspect_object, transform_name def create_subparser_class(opts_parser): # This is a hack to add the main parser arguments to each subcommand in # order to allow main parser arguments to be specified after the # subcommand, e.g. # my_prog status -t --atonce=10 # # The rationale of the implementation chosen is to propagate mutually # exclusive groups from main parser to subparsers. While it is possible # to infer kwargs from main parser actions list then passing them to the # add_argument() method for each subparser, it will make us lose any # information about mutually exclusive groups. class SubParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): kwargs["add_help"] = False super(SubParser, self).__init__(*args, **kwargs) self._copied_actions_fingerprints = set() # Copy mutually exclusive groups first self._copy_mutually_exclusive_groups() # Obviously we care only about optionals self._copy_optionals() def _copy_action(self, action, group, default=argparse.SUPPRESS): action_fingerprint = "".join(action.option_strings) # Avoid adding same option twice if action_fingerprint not in self._copied_actions_fingerprints: # FIXME: this is a really, really bad idea a = copy.copy(action) # Avoid common arguments to be overridden by subnamespace a.default = default group._add_action(a) self._copied_actions_fingerprints.add(action_fingerprint) def _copy_mutually_exclusive_groups(self): for mutex_group in opts_parser._mutually_exclusive_groups: mutex_group_copy = self.add_mutually_exclusive_group( required=mutex_group.required ) for action in mutex_group._group_actions: self._copy_action(action, mutex_group_copy) def _copy_optionals(self): for action in opts_parser._optionals._actions: # Skip _SubParsersAction from main parser if not isinstance(action, argparse._SubParsersAction): self._copy_action(action, self._optionals) return SubParser def add_command(argparse_parser, function): inspection = inspect_object(function) if not inspection.command: return add_command(argparse_parser, command(function)) parser = register_command(argparse_parser, inspection) # put a back reference so we can find this function later on `find_command` # used in testing parser.__command = function return parser def register_command(argparse_parser, inspection): _command = inspection.command # auto wrap the function with @command in case its not wrapped into one subparsers = _resolve_subparsers(argparse_parser) subparser = subparsers.add_parser( _command.name, aliases=_command.aliases, help=_command.help ) # Exclusive arguments needs to be added to argparse's mutually exclusive # groups exclusive_args = _command.exclusive_arguments or [] mutually_exclusive_groups = defaultdict(subparser.add_mutually_exclusive_group) for arg in inspection.arguments.values(): add_argument_args, add_argument_kwargs = _argument_to_argparse_input(arg) groups = [group for group in exclusive_args if arg.name in group] if isinstance(add_argument_kwargs.get('choices', []), Callable): add_argument_kwargs.pop('choices') if not groups: subparser.add_argument(*add_argument_args, **add_argument_kwargs) elif len(groups) == 1: me_group = mutually_exclusive_groups[groups[0]] me_group.add_argument(*add_argument_args, **add_argument_kwargs) elif len(groups) > 1: msg = ( "Argument {} is present in more than one exclusive " "group: {}. This should not be allowed by the @command " "decorator".format(arg.name, groups) ) raise ValueError(msg) # if we are adding a super command then we need to create a sub parser for # this if len(inspection.subcommands) > 0: subcommand_parsers = subparser.add_subparsers( dest="_subcmd", help=_command.help, parser_class=create_subparser_class(subparser), metavar="[subcommand]".format(_command.name), ) subcommand_parsers.required = True # recursively add sub-commands for _, v in inspection.subcommands: register_command(subcommand_parsers, v) return subparser def _resolve_subparsers(parser): # a subparser resulting from parser.add_subparsers was inputted if isinstance(parser, argparse._SubParsersAction): subparsers = parser # an actual parser was inputted elif isinstance(parser, argparse.ArgumentParser): # Unfortunately there is no method to get the current subparsers apart # from reading the private property. Trying to call # parser.add_subparsers a second time will result in a SystemExit error. # Also when you call parser.add_subparsers you get an Action object, # that is listed under parser._subparsers._actions. # Argparse is a beautiful thing if getattr(parser, "_subparsers", None): subparsers = parser._subparsers._actions[-1] else: subparsers = parser.add_subparsers(dest="_cmd", help="Subcommand to run") else: raise ValueError( "Expected an argparse.ArgumentParser or an " "argparse._SubParsersAction as input" ) return subparsers def _argument_to_argparse_input(arg: "Any") -> "Tuple[List, Dict[str, Any]]": add_argument_kwargs = {"help": arg.description} if arg.positional: add_argument_args = [arg.name] if arg.extra_names: msg = "Aliases are not yet supported for positional arguments @ {}".format( arg.name ) raise ValueError(msg) if arg.default_value_set: msg = ( "Positional arguments with default values are " "not supported @ {}".format(arg.name) ) raise ValueError(msg) else: add_argument_args = [ transform_argument_name(x) for x in ([arg.name] + arg.extra_names) ] add_argument_kwargs["default"] = arg.default_value add_argument_kwargs["required"] = not arg.default_value_set argument_type = ( arg.type if not is_optional_type(arg.type) else get_first_type_argument(arg.type) ) if argument_type in [int, float, str]: add_argument_kwargs["type"] = argument_type add_argument_kwargs["metavar"] = str(argument_type.__name__).upper() elif argument_type == bool or arg.default_value is False: add_argument_kwargs["action"] = "store_true" elif arg.default_value is True: add_argument_kwargs["action"] = "store_false" elif is_mapping_type(argument_type): add_argument_kwargs["type"] = _parse_dict(argument_type) add_argument_kwargs["metavar"] = "DICT[{}: {}]".format( *get_dict_kv_arg_type_as_str(argument_type) ) elif is_iterable_type(argument_type): add_argument_kwargs["type"] = get_first_type_argument(argument_type) add_argument_kwargs["nargs"] = "+" add_argument_kwargs["metavar"] = "{}".format( get_list_arg_type_as_str(argument_type) ) else: add_argument_kwargs["type"] = argument_type if arg.choices: add_argument_kwargs["choices"] = arg.choices if not isinstance(arg.choices, Callable): add_argument_kwargs["metavar"] = "{{{}}}".format( ",".join(map(str, arg.choices)) ) if arg.positional and "metavar" in add_argument_kwargs: add_argument_kwargs["metavar"] = "{}<{}>".format( arg.name, add_argument_kwargs["metavar"] ) return add_argument_args, add_argument_kwargs def find_command(parser, parsed_args, curry_args=False): subparsers = _resolve_subparsers(parser) parser_map = dict(item for item in subparsers._name_parser_map.items()) parser = parser_map.get(parsed_args._cmd) function = parser.__command if parser else None if not function: return None if curry_args: kwargs = get_arguments_for_command(function, parsed_args) function = partial(function, **kwargs) return function def get_arguments_for_inspection(inspection, kwargs): # map back from names or extra names given to arguments to the actual # arguments taken by the function names_to_args = { transform_name(arg_obj.name, to_char="_"): arg_obj.arg for arg, arg_obj in inspection.arguments.items() } names_to_args.update( { transform_name(extra_name, to_char="_"): arg_obj.arg for arg, arg_obj in inspection.arguments.items() for extra_name in arg_obj.extra_names } ) # disconsider _cmd as it is used to identify the function/parser, not the # actual arguments valid_args = set(map(lambda arg_obj: arg_obj.arg, inspection.arguments.values())) # use the reverse map to convert the names used in parsing to the actual # arguments used in the command function # filter out any argument that is not accepted by this function. kwargs = { names_to_args.get(name, name): value for name, value in kwargs.items() if names_to_args.get(name, name) in valid_args } return kwargs def get_arguments_for_command(function, parsed_args): # map back from names or extra names given to arguments to the actual # arguments taken by the function inspection = inspect_object(function) kwargs = dict(parsed_args._get_kwargs()) return get_arguments_for_inspection(inspection, kwargs) def transform_argument_name(name): """ Similar to transform_name, this is specific to export argument names for the cli mode. Single character friendly names are treated as flags and have a single dash (-) instead of a double dash (--) For instance: __special__ => --special _some_arg = --some-arg _f => -f """ name = transform_name(name) return "--{}".format(name) if len(name) > 1 else "-{}".format(name) def _parse_dict(target_type): def parse_dict_(value): return build_value(value, target_type, python_syntax=False) return parse_dict_ class NubiaHelpAction(argparse.Action): """An action that pipes help message to the pager.""" def __init__( self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None, ): super(NubiaHelpAction, self).__init__( option_strings=option_strings, dest=dest, default=default, nargs=0, help=help, ) def __call__(self, parser, namespace, values, option_string=None): help_message = parser.format_help() help_message_length = len(help_message.split("\n")) _, rows = shutil.get_terminal_size() fits_one_page = help_message_length <= rows if sys.stdout.isatty() and not fits_one_page: pager = os.environ.get("PAGER", "less") subprocess.run([pager], input=help_message.encode()) else: # fallback parser.print_help() parser.exit() python-nubia-0.2.3/nubia/internal/typing/builder.py000066400000000000000000000135371434345131700223730ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import ast import re import sys import typing from functools import wraps from nubia.internal.helpers import issubclass_ from nubia.internal.typing.inspect import ( NEW_TYPING, is_iterable_type, is_mapping_type, is_optional_type, is_tuple_type, is_typevar, ) def build_value(string, tp=None, python_syntax=False): value = _safe_eval(string) if python_syntax else _build_simple_value(string, tp) if tp: value = apply_typing(value, tp) return value def apply_typing(value, tp): return get_typing_function(tp)(value) def get_list_arg_type_as_str(tp): """ This takes a type (typing.List[int]) and returns a string representation of the type argument, or "any" if it's not defined """ assert is_iterable_type(tp) args = getattr(tp, "__args__", None) return args[0].__name__ if args else "any" def is_dict_value_iterable(tp): assert is_mapping_type(tp), f"{tp} is not a mapping type" args = getattr(tp, "__args__", None) if args and len(args) == 2: return is_iterable_type(args[1]) return False def get_dict_kv_arg_type_as_str(tp): """ This takes a type (typing.Mapping[str, int]) and returns a tuple (key_type, value_type) that contains string representations of the type arguments, or "any" if it's not defined """ assert is_mapping_type(tp), f"{tp} is not a mapping type" args = getattr(tp, "__args__", None) key_type = "any" value_type = "any" if args and len(args) >= 2: key_type = getattr(args[0], "__name__", str(args[0])) value_type = getattr(args[1], "__name__", str(args[1])) return key_type, value_type def get_typing_function(tp): func = None # TypeVars are a problem as they can defined multiple possible types. # While a single type TypeVar is somewhat useless, no reason to deny it # though if is_typevar(tp): if len(tp.__constraints__) == 0: # Unconstrained TypeVars may come from generics func = _identity_function elif len(tp.__constraints__) == 1: assert not NEW_TYPING, "Python 3.7+ forbids single constraint for `TypeVar'" func = get_typing_function(tp.__constraints__[0]) else: raise ValueError( "Cannot resolve typing function for TypeVar({constraints}) " "as it declares multiple types".format( constraints=", ".join( getattr(c, "_name", c.__name__) for c in tp.__constraints__ ) ) ) elif tp == typing.Any: func = _identity_function elif issubclass_(tp, str): func = str elif is_mapping_type(tp): func = _apply_dict_type elif is_tuple_type(tp): func = _apply_tuple_type elif is_iterable_type(tp): func = _apply_list_type elif is_optional_type(tp): func = _apply_optional_type elif callable(tp): func = tp else: raise ValueError('Cannot find a function to apply type "{}"'.format(tp)) args = getattr(tp, "__args__", None) if args: # this can be a Generic type from the typing module, like # List[str], Mapping[int, str] and so on. In that case we need to # also deal with the generic typing args_types = [get_typing_function(arg) for arg in args] func = _partial_builder(args_types)(func) return func def _safe_eval(string): try: return ast.literal_eval(string) except ValueError as e: _, e, tb = sys.exc_info() if str(e) == "malformed string": # Raise a more meaningful, nicer error raise ValueError(f"`{string}' uses unsafe token/symbols") from e else: raise def _build_simple_value(string, tp): if not tp or issubclass_(tp, str): return string elif is_mapping_type(tp): entries = ( re.split(r"\s*[:=]\s*", entry, maxsplit=1) for entry in string.split(";") ) if is_dict_value_iterable(tp): entries = ((k, re.split(r"\s*,\s*", v)) for k, v in entries) return {k.strip(): v for k, v in entries} elif is_tuple_type(tp): return tuple(item for item in string.split(",")) elif is_iterable_type(tp): return [item for item in string.split(",")] else: return string def _apply_dict_type(value, key_type=None, value_type=None): if not key_type and not value_type: return dict(value) key_type = key_type or _identity_function value_type = value_type or _identity_function return {key_type(key): value_type(value) for key, value in value.items()} def _apply_tuple_type(value, *types): if not types: return tuple(value) if len(value) != len(types): raise ValueError( "Cannot build a tuple of {} elements with {} " 'values: "{}"'.format(len(types), len(value), value) ) return tuple(function(value) for function, value in zip(types, value)) def _apply_list_type(value, value_type=None): if not isinstance(value, list): value = [value] if not value_type: return list(value) return [value_type(item) for item in value] def _apply_optional_type(value, left_type=None, _right_type=None): if value is None: return None elif left_type is None: return value else: return left_type(value) def _partial_builder(args_builders): def decorator(function): @wraps(function) def wrapped(string): return function(string, *args_builders) return wrapped return decorator def _identity_function(x): return x python-nubia-0.2.3/nubia/internal/typing/inspect.py000066400000000000000000000032761434345131700224110ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import collections.abc from typing import Iterable, List, Mapping # This is re-exported, add more if you need more from typing_inspect elsewhere. from typing_inspect import is_optional_type # noqa from typing_inspect import NEW_TYPING, is_tuple_type, is_typevar, is_union_type from nubia.internal.helpers import issubclass_ if NEW_TYPING: from typing import _GenericAlias def _is_generic_alias_of(this, that) -> bool: return isinstance(this, _GenericAlias) and issubclass_(this.__origin__, that) def is_none_type(tp) -> bool: """Checks whether a type is a `None' type.""" return tp is type(None) # noqa E721 def is_mapping_type(tp) -> bool: """Checks whether a type is a mapping type.""" if NEW_TYPING: return tp is Mapping or _is_generic_alias_of(tp, collections.abc.Mapping) return issubclass_(tp, collections.abc.Mapping) def is_iterable_type(tp) -> bool: """Checks whether a type is an iterable type.""" if NEW_TYPING: return tp is Iterable or tp is List or _is_generic_alias_of(tp, collections.abc.Iterable) return issubclass_(tp, list) def get_first_type_argument(tp): """Returns first type argument, e.g. `int' for `List[int]'.""" assert hasattr(tp, "__args__") and len(tp.__args__) > 0 return tp.__args__[0] def is_list_type(tp) -> bool: """Checks whether a type is a typing.List.""" if NEW_TYPING: return tp is List or _is_generic_alias_of(tp, list) return issubclass_(tp, list) python-nubia-0.2.3/nubia/internal/ui/000077500000000000000000000000001434345131700174655ustar00rootroot00000000000000python-nubia-0.2.3/nubia/internal/ui/__init__.py000066400000000000000000000003501434345131700215740ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/nubia/internal/ui/ipython.py000066400000000000000000000013201434345131700215250ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from IPython.terminal.prompts import Prompts, Token class NubiaPrompt(Prompts): def in_prompt_tokens(self, cli=None): return [ (Token.Prompt, "["), (Token.PromptNum, str(self.shell.execution_count)), (Token.Prompt, "] "), ] def out_prompt_tokens(self): return [ (Token.OutPrompt, "Out<"), (Token.OutPromptNum, str(self.shell.execution_count)), (Token.OutPrompt, ">: "), ] python-nubia-0.2.3/nubia/internal/ui/lexer.py000066400000000000000000000232221434345131700211570ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import re from pygments.lexer import RegexLexer, bygroups from pygments.token import Keyword, Name, Number, Operator, Punctuation, String, Text from nubia import context from nubia.internal import parser _identifier = r"[a-zA-Z_][a-zA-Z0-9_\-]*" _unquoted_string = "([a-zA-Z0-9{}]+)".format( re.escape(parser.allowed_symbols_in_string) ) _command = r"(:?[a-zA-Z_][a-zA-Z0-9_\-]*)" def command_callback(lexer, match): """ When matching a command, the lexer would look up the command registry to decide on how to highlight the command. We will emit Name.Command if this is a valid command, otherwise we emit Text. We also take care of the sub-commands if this is a super command. Otherwise, we consider the second argument a positional argument in this case. """ command = match.group(1) # We do need to know whether we are parsing two groups (command) or four # (command with subcommand) command_with_argument = len(match.groups()) > 2 ctx = context.get_context() cmd = ctx.registry.find_command(command.strip()) # We know this command command_token = Name.InvalidCommand subcommand_token = Name.InvalidCommand if cmd: command_token = Name.Command # Now, let's see if this is a super command or not. if command_with_argument: if cmd.super_command: # That's a sub-command, is this a valid sub-command? subcmd = match.group(3) if cmd.has_subcommand(subcmd): subcommand_token = Name.SubCommand else: # Just a positional subcommand_token = Name.Symbol yield (match.start(1), command_token, command) # matches the spaces yield (match.start(2), Text, match.group(2)) if command_with_argument: yield (match.start(3), subcommand_token, match.group(3)) # matches the spaces yield (match.start(4), Text, match.group(4)) class NubiaLexer(RegexLexer): name = "Nubia Interactive Lexer" filenames = ["*.nubia"] flags = re.IGNORECASE tokens = { str("root"): [ # We want to change the state of the lexer if the first word is # select so that we can have the sql lexer (r"^SELECT\s", Name.Command, str("query")), (r"\s+", Text), (r"^(\?|help)\s*$", Name.Help), (r"^(q|quit|exit)\s*$", Name.Exit), # Command with Subcommands (r"(" + _identifier + r")(\s*=\s*)", bygroups(Name.Key, Operator)), # Commands (r"^" + _command + r"(\s+)" + _command + r"(\s+)", command_callback), (r"^" + _command + r"(\s+|$)", command_callback), (r"(" + _identifier + r")(\s*)", Name.Symbol), (r"(True|False|true|false)", Keyword), (r"\-?[0-9]+", Number.Integer), (r"'(''|[^'])*'", String.Single), # not a real string literal in ANSI SQL (r'"(""|[^"])*"', String.Symbol), (r"[;:()\[\],\.]", Punctuation), ], str("query"): [ (r"\s+", Text), ( r"(ABORT|ABS|ABSOLUTE|ACCESS|ADA|ADD|ADMIN|AFTER|AGGREGATE|" r"ALIAS|ALL|ALLOCATE|ALTER|ANALYSE|ANALYZE|AND|ANY|ARE|AS|" r"ASC|ASENSITIVE|ASSERTION|ASSIGNMENT|ASYMMETRIC|AT|ATOMIC|" r"AUTHORIZATION|AVG|BACKWARD|BEFORE|BEGIN|BETWEEN|BITVAR|" r"BIT_LENGTH|BOTH|BREADTH|BY|C|CACHE|CALL|CALLED|CARDINALITY|" r"CASCADE|CASCADED|CASE|CAST|CATALOG|CATALOG_NAME|CHAIN|" r"CHARACTERISTICS|CHARACTER_LENGTH|CHARACTER_SET_CATALOG|" r"CHARACTER_SET_NAME|CHARACTER_SET_SCHEMA|CHAR_LENGTH|CHECK|" r"CHECKED|CHECKPOINT|CLASS|CLASS_ORIGIN|CLOB|CLOSE|CLUSTER|" r"COALSECE|COBOL|COLLATE|COLLATION|COLLATION_CATALOG|" r"COLLATION_NAME|COLLATION_SCHEMA|COLUMN|COLUMN_NAME|" r"COMMAND_FUNCTION|COMMAND_FUNCTION_CODE|COMMENT|COMMIT|" r"COMMITTED|COMPLETION|CONDITION_NUMBER|CONNECT|CONNECTION|" r"CONNECTION_NAME|CONSTRAINT|CONSTRAINTS|CONSTRAINT_CATALOG|" r"CONSTRAINT_NAME|CONSTRAINT_SCHEMA|CONSTRUCTOR|CONTAINS|" r"CONTINUE|CONVERSION|CONVERT|COPY|CORRESPONTING|COUNT|" r"CREATE|CREATEDB|CREATEUSER|CROSS|CUBE|CURRENT|CURRENT_DATE|" r"CURRENT_PATH|CURRENT_ROLE|CURRENT_TIME|CURRENT_TIMESTAMP|" r"CURRENT_USER|CURSOR|CURSOR_NAME|CYCLE|DATA|DATABASE|" r"DATETIME_INTERVAL_CODE|DATETIME_INTERVAL_PRECISION|DAY|" r"DEALLOCATE|DECLARE|DEFAULT|DEFAULTS|DEFERRABLE|DEFERRED|" r"DEFINED|DEFINER|DELETE|DELIMITER|DELIMITERS|DEREF|DESC|" r"DESCRIBE|DESCRIPTOR|DESTROY|DESTRUCTOR|DETERMINISTIC|" r"DIAGNOSTICS|DICTIONARY|DISCONNECT|DISPATCH|DISTINCT|DO|" r"DOMAIN|DROP|DYNAMIC|DYNAMIC_FUNCTION|DYNAMIC_FUNCTION_CODE|" r"EACH|ELSE|ENCODING|ENCRYPTED|END|END-EXEC|EQUALS|ESCAPE|EVERY|" r"EXCEPT|ESCEPTION|EXCLUDING|EXCLUSIVE|EXEC|EXECUTE|EXISTING|" r"EXISTS|EXPLAIN|EXTERNAL|EXTRACT|FALSE|FETCH|FINAL|FIRST|FOR|" r"FORCE|FOREIGN|FORTRAN|FORWARD|FOUND|FREE|FREEZE|FROM|FULL|" r"FUNCTION|G|GENERAL|GENERATED|GET|GLOBAL|GO|GOTO|GRANT|GRANTED|" r"GROUP|GROUPING|HANDLER|HAVING|HIERARCHY|HOLD|HOST|IDENTITY|" r"IGNORE|ILIKE|IMMEDIATE|IMMUTABLE|IMPLEMENTATION|IMPLICIT|IN|" r"INCLUDING|INCREMENT|INDEX|INDITCATOR|INFIX|INHERITS|INITIALIZE|" r"INITIALLY|INNER|INOUT|INPUT|INSENSITIVE|INSERT|INSTANTIABLE|" r"INSTEAD|INTERSECT|INTO|INVOKER|IS|ISNULL|ISOLATION|ITERATE|JOIN|" r"KEY|KEY_MEMBER|KEY_TYPE|LANCOMPILER|LANGUAGE|LARGE|LAST|" r"LATERAL|LEADING|LEFT|LENGTH|LESS|LEVEL|LIKE|LIMIT|LISTEN|LOAD|" r"LOCAL|LOCALTIME|LOCALTIMESTAMP|LOCATION|LOCATOR|LOCK|LOWER|" r"MAP|MATCH|MAX|MAXVALUE|MESSAGE_LENGTH|MESSAGE_OCTET_LENGTH|" r"MESSAGE_TEXT|METHOD|MIN|MINUTE|MINVALUE|MOD|MODE|MODIFIES|" r"MODIFY|MONTH|MORE|MOVE|MUMPS|NAMES|NATIONAL|NATURAL|NCHAR|" r"NCLOB|NEW|NEXT|NO|NOCREATEDB|NOCREATEUSER|NONE|NOT|NOTHING|" r"NOTIFY|NOTNULL|NULL|NULLABLE|NULLIF|OBJECT|OCTET_LENGTH|OF|OFF|" r"OFFSET|OIDS|OLD|ON|ONLY|OPEN|OPERATION|OPERATOR|OPTION|OPTIONS|" r"OR|ORDER|ORDINALITY|OUT|OUTER|OUTPUT|OVERLAPS|OVERLAY|OVERRIDING|" r"OWNER|PAD|PARAMETER|PARAMETERS|PARAMETER_MODE|PARAMATER_NAME|" r"PARAMATER_ORDINAL_POSITION|PARAMETER_SPECIFIC_CATALOG|" r"PARAMETER_SPECIFIC_NAME|PARAMATER_SPECIFIC_SCHEMA|PARTIAL|" r"PASCAL|PENDANT|PLACING|PLI|POSITION|POSTFIX|PRECISION|PREFIX|" r"PREORDER|PREPARE|PRESERVE|PRIMARY|PRIOR|PRIVILEGES|PROCEDURAL|" r"PROCEDURE|PUBLIC|READ|READS|RECHECK|RECURSIVE|REF|REFERENCES|" r"REFERENCING|REINDEX|RELATIVE|RENAME|REPEATABLE|REPLACE|RESET|" r"RESTART|RESTRICT|RESULT|RETURN|RETURNED_LENGTH|" r"RETURNED_OCTET_LENGTH|RETURNED_SQLSTATE|RETURNS|REVOKE|RIGHT|" r"ROLE|ROLLBACK|ROLLUP|ROUTINE|ROUTINE_CATALOG|ROUTINE_NAME|" r"ROUTINE_SCHEMA|ROW|ROWS|ROW_COUNT|RULE|SAVE_POINT|SCALE|SCHEMA|" r"SCHEMA_NAME|SCOPE|SCROLL|SEARCH|SECOND|SECURITY|SELECT|SELF|" r"SENSITIVE|SERIALIZABLE|SERVER_NAME|SESSION|SESSION_USER|SET|" r"SETOF|SETS|SHARE|SHOW|SIMILAR|SIMPLE|SIZE|SOME|SOURCE|SPACE|" r"SPECIFIC|SPECIFICTYPE|SPECIFIC_NAME|SQL|SQLCODE|SQLERROR|" r"SQLEXCEPTION|SQLSTATE|SQLWARNINIG|STABLE|START|STATE|STATEMENT|" r"STATIC|STATISTICS|STDIN|STDOUT|STORAGE|STRICT|STRUCTURE|STYPE|" r"SUBCLASS_ORIGIN|SUBLIST|SUBSTRING|SUM|SYMMETRIC|SYSID|SYSTEM|" r"SYSTEM_USER|TABLE|TABLE_NAME| TEMP|TEMPLATE|TEMPORARY|TERMINATE|" r"THAN|THEN|TIMESTAMP|TIMEZONE_HOUR|TIMEZONE_MINUTE|TO|TOAST|" r"TRAILING|TRANSATION|TRANSACTIONS_COMMITTED|" r"TRANSACTIONS_ROLLED_BACK|TRANSATION_ACTIVE|TRANSFORM|" r"TRANSFORMS|TRANSLATE|TRANSLATION|TREAT|TRIGGER|TRIGGER_CATALOG|" r"TRIGGER_NAME|TRIGGER_SCHEMA|TRIM|TRUE|TRUNCATE|TRUSTED|TYPE|" r"UNCOMMITTED|UNDER|UNENCRYPTED|UNION|UNIQUE|UNKNOWN|UNLISTEN|" r"UNNAMED|UNNEST|UNTIL|UPDATE|UPPER|USAGE|USER|" r"USER_DEFINED_TYPE_CATALOG|USER_DEFINED_TYPE_NAME|" r"USER_DEFINED_TYPE_SCHEMA|USING|VACUUM|VALID|VALIDATOR|VALUES|" r"VARIABLE|VERBOSE|VERSION|VIEW|VOLATILE|WHEN|WHENEVER|WHERE|" r"WITH|WITHOUT|WORK|WRITE|YEAR|ZONE)\b", Keyword, ), ( r"(ARRAY|BIGINT|BINARY|BIT|BLOB|BOOLEAN|CHAR|CHARACTER|DATE|" r"DEC|DECIMAL|FLOAT|INT|INTEGER|INTERVAL|NUMBER|NUMERIC|REAL|" r"SERIAL|SMALLINT|VARCHAR|VARYING|INT8|SERIAL8|TEXT)\b", Name.Builtin, ), (r"[+*/<>=~!@#%^&|`?-]", Operator), (r"[0-9]+", Number.Integer), (r"'(''|[^'])*'", String.Single), # not a real string literal in ANSI SQL (r'"(""|[^"])*"', String.Symbol), (r"[a-zA-Z_][a-zA-Z0-9_]*", Name), (r"[;:()\[\],\.]", Punctuation), ], } python-nubia-0.2.3/nubia/internal/ui/statusbar.py000066400000000000000000000013011434345131700220420ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia.internal.io import eventbus class StatusBar(eventbus.Listener): def __init__(self, context): pass async def on_connected(self, *args, **kwargs): """ Do nothing by default. """ pass def get_rprompt_tokens(self): return [] def set_last_command_status(self, status): pass def get_tokens(self): return [] def start(self): pass def stop(self): pass python-nubia-0.2.3/nubia/internal/ui/style.py000066400000000000000000000044311434345131700212010ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from prompt_toolkit.styles import ( Style, merge_styles, style_from_pygments_cls, style_from_pygments_dict, ) from pygments.styles.monokai import MonokaiStyle from pygments.token import Name, Token shell_style = merge_styles( [ style_from_pygments_cls(MonokaiStyle), style_from_pygments_dict( { # Commands Name.Command: "#f2b44f", Name.SubCommand: "#f2c46f", Name.InvalidCommand: "bg:#ff0066 #000000", Name.Select: "#0000ff", Name.Query: "#d78700", Name.Key: "#ffffff", Name.Path: "#fff484", Name.Help: "#00aa00", Name.Exit: "#ff0066", # User input. Token: "#ff0066", # Prompt. Token.Username: "#884444", Token.At: "#00aa00", Token.Colon: "#00aa00", Token.Pound: "#00aa00", Token.Tier: "#ff0088", Token.Path: "#884444 underline", Token.RPrompt: "bg:#ff0066 #ffffff", # Toolbar Tokens Token.Toolbar: "#ffffff bg:#1c1c1c", Token.TestTier: "#ff0000 bg:#1c1c1c", Token.ProductionTier: "#ff0000 bg:#1c1c1c", Token.OfflineNodes: "#ff0000 bg:#1c1c1c", Token.NodesCount: "#ffffff bg:#1c1c1c", Token.Spacer: "#ffffff bg:#1c1c1c", # Alarms Token.MinorAlarm: "#0000ff bg:#1c1c1c", Token.MajorAlarm: "#d78700 bg:#1c1c1c", Token.CriticalAlarm: "#ff0000 bg:#1c1c1c", Token.AppendFailures: "#0000ff bg:#1c1c1c", # General Token.Good: "#ffffff bg:#10c010", Token.Bad: "#ffffff bg:#c01010", Token.Info: "#ffffff bg:#1010c0", Token.Warn: "#000000 bg:#c0c010", } ), Style.from_dict({"bottom-toolbar": "fg:#ffffff bg:#1c1c1c noinherit"}), ] ) python-nubia-0.2.3/nubia/internal/usage_logger_interface.py000066400000000000000000000020531434345131700241050ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # class UsageLoggerInterface: """ The UsageLoggerInterface class is a way to customize nubia usage logging to your logging infrastructure. If active, the UsageLogger is notified on all command executions, which allows tracking of usage stats like number of command executions, runtimes, success rates, used parameters and more. """ def __init__(self, context): """ Init your logger here. """ pass def pre_exec(self): """ Called before every command execution. Can be used to measure how long tasks take to execute. """ pass def post_exec(self, cmd, params, result, is_cli): """ Called after every command execution. Use this for timing and logging the execution results. """ pass python-nubia-0.2.3/nubia_complete/000077500000000000000000000000001434345131700171245ustar00rootroot00000000000000python-nubia-0.2.3/nubia_complete/__init__.py000066400000000000000000000003501434345131700212330ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # python-nubia-0.2.3/nubia_complete/completer.py000066400000000000000000000154101434345131700214710ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import json import logging import os import re import shlex import string logger = logging.getLogger(__name__) option_regex = re.compile("(?P\-\-?[\w\-]+\=)") def run_complete(args): model_file = args.command_model_path logging.info("Command model: %s", model_file) comp_line = os.getenv("COMP_LINE") comp_point = int(os.getenv("COMP_POINT", "0")) comp_type = os.getenv("COMP_TYPE") comp_shell = os.getenv("COMP_SHELL", "bash") if not comp_line: logger.error("$COMP_LINE is unset, failing!") return 1 if not comp_point: logger.error("$COMP_POINT is unset, failing!") return 1 # Fix the disparity between zsh and bash for COMP_POINT if comp_shell == "zsh": comp_point -= 1 # We want to trim the remaining of the line because we don't care about it comp_line = comp_line[:comp_point] # We want to tokenize the input using these rules: # - Separate by space unless there it's we are in " or ' try: tokens = shlex.split(comp_line) if len(tokens) < 1: return 1 # drop the first word (the executable name) tokens = tokens[1:] except ValueError: logger.warning("We are in an open quotations, cannot suggestion completions") return 0 logger.debug("COMP_LINE: @%s@", comp_line) logger.debug("COMP_POINT: %s", comp_point) logger.debug("COMP_TYPE: %s", comp_type) logger.debug("COMP_SHELL: %s", comp_shell) # we want to know if the cursor is on a space or a word. If it's on a space, # then we expect a completion of (command, option, or value). current_token = None if comp_line[comp_point - 1] not in string.whitespace: current_token = tokens[-1] tokens = tokens[:-1] logger.debug("Input Tokens: %s", tokens) logger.debug("Current token: %s", current_token) # loading the model with open(model_file, "r") as f: model = json.load(f) completions = get_completions(model, tokens, current_token, comp_shell) for completion in completions: logger.debug("Completion: @%s@", completion) print(completion) def _drop_from_options(options, token, skip_value=False): # does this token in the format "-[-]x=" ? tokens = token.split("=") if skip_value: tokens = tokens[:1] for i, option in enumerate(options): logger.debug("Tokens: %s", tokens) if tokens[0] == option.get("name") or tokens[0] in option.get("extra_names"): logger.debug("Dropping option %s", option) if option.get("expects_argument"): if len(tokens) > 1: # we have the argument already options.pop(i) return None return options.pop(i) else: return None else: logger.debug("mismatch: %s and %s", option.get("name"), tokens[0]) def _get_values_for_option(option, prefix=""): logger.debug("Should auto-complete for option %s", option.get("name")) output = option.get("values", []) if output: output = [prefix + _space_suffix(k) for k in output] logger.debug("Values: %s", output) return output def get_completions(model, tokens, current, shell): output = [] options_we_expect = model["options"] current_command_list = model.get("commands", []) last_option_found = None for token in tokens: if token.startswith("-"): # it's an option, drop it from expected current_option = _drop_from_options(options_we_expect, token) if current_option and current_option.get("expects_argument"): last_option_found = current_option else: # this is: # - Argument to an option (ignore) # - Command # - Some random free argument if last_option_found: # does it expect a value? logger.debug( "Skipping %s because it's an argument to %s", token, last_option_found.get("name"), ) last_option_found = None continue last_option_found = None for command in current_command_list: if token == command.get("name"): logger.debug("We matched command %s", command.get("name")) options_we_expect.extend(command.get("options", [])) # for sub-commands current_command_list = command.get("commands", []) break else: logger.debug( "We didn't find any matching command, ignoring the token %s", token, ) # Now that we know where we are, let's complete the current token: if last_option_found: # we are expecting a value for this output = _get_values_for_option(last_option_found) else: # If the current token is '--something=' then we should try to # autocomplete a value for this if current: match = option_regex.match(current) if match: key = match.groupdict()["key"] logger.debug("We are in a value-completion inside %s", key) # it's true option = _drop_from_options(options_we_expect, current, skip_value=True) if option: # YES, we have it, let's get the values prefix = "" if shell == "zsh": # in zsh, we need to prepend the completions with the # key prefix = key return _get_values_for_option(option, prefix) output.extend(_completions_for_options(options_we_expect)) output.extend(_completions_for_commands(current_command_list)) return output def _space_suffix(word): return word + " " def _completions_for_options(options): output = [] should_suffix = int(os.getenv("NUBIA_SUFFIX_ENABLED", "1")) def __suffix(key, expects_argument=True): if should_suffix and expects_argument: return key + "=" else: return _space_suffix(key) for option in options: expects_argument = False if option.get("expects_argument"): expects_argument = True output.append(__suffix(option.get("name"), expects_argument)) return output def _completions_for_commands(commands): return [_space_suffix(x["name"]) for x in commands] python-nubia-0.2.3/nubia_complete/main.py000066400000000000000000000041031434345131700204200ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse import logging import sys from nubia_complete.completer import run_complete from nubia_complete.shell import generate_shell_setup logger = logging.getLogger(__name__) def main(): sys.exit(run(sys.argv)) def run(args): opts_parser = argparse.ArgumentParser( description="A shell completion utility for nubia programs" ) subparsers = opts_parser.add_subparsers(help="sub-command help", dest="mode") opts_parser.add_argument( "--loglevel", type=str, default="INFO", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], help="logging level", ) generate_parser = subparsers.add_parser( "generate-shell-setup", help="Generates a bash/zsh setup script that you can source", ) complete_parser = subparsers.add_parser("complete", help="Triggers completions") generate_parser.add_argument( "--target-binary-name", type=str, required=True, help="The name of the nubia program we want to generate a completer for", ) generate_parser.add_argument( "--command-model-path", type=str, required=True, help="The location on which to find the command model", ) complete_parser.add_argument( "--command-model-path", type=str, required=True, help="The location on which to find the command model", ) args = opts_parser.parse_args() # Setting up logging log_level = logging.getLevelName(args.loglevel) logging.basicConfig(level=log_level) if args.mode == "generate-shell-setup": return generate_shell_setup(args.target_binary_name, args.command_model_path) elif args.mode == "complete": return run_complete(args) else: print("Not Implemented!") return 2 if __name__ == "__main__": main() python-nubia-0.2.3/nubia_complete/shell.py000066400000000000000000000041421434345131700206060ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import re import string import sys regex = re.compile("[{}]".format(re.escape(string.punctuation))) def generate_shell_setup(command_name, command_model): clean_command_name = regex.sub("_", command_name) func_name = "_nubia_completer_{}".format(clean_command_name) template = """ if [[ -n ${{ZSH_VERSION-}} ]]; then # zsh setup _zsh_{func_name}() {{ local nubia_completer=${{NUBIA_COMPLETER_BINARY:-"{completer}"}} local log_level=${{NUBIA_COMPLETER_LOG_LEVEL:-"INFO"}} local log_file=${{NUBIA_COMPLETER_LOG_FILE:-"/dev/null"}} local word completions local IFS=$'\\n' read -l; local cl="$REPLY"; read -ln; local cp="$REPLY"; reply=(`COMP_SHELL="zsh" \\ COMP_LINE="$cl" \\ COMP_POINT="$cp" \\ $nubia_completer --loglevel ${{log_level}} complete \\ --command-model-path="{model}" \\ 2>> "$log_file"`) }} compctl -Q -S '' -K _zsh_{func_name} "{command}" else # bash setup _bash_{func_name}() {{ local nubia_completer=${{NUBIA_COMPLETER_BINARY:-"{completer}"}} local log_level=${{NUBIA_COMPLETER_LOG_LEVEL:-"INFO"}} local log_file=${{NUBIA_COMPLETER_LOG_FILE:-"/dev/null"}} COMPREPLY=() local word="$2" local IFS=$'\\n' local completions="$(COMP_LINE="$COMP_LINE" \\ COMP_WORDS="${{COMP_WORDS[1]}}" \\ COMP_POINT="$COMP_POINT" \\ COMP_TYPE="$COMP_TYPE" \\ COMP_WORDBREAKS="$COMP_WORDBREAKS" \\ $nubia_completer --loglevel "${{log_level}}" complete \\ --command-model-path="{model}" \\ 2>> "$log_file")" COMPREPLY=( $(compgen -W "$completions" -- "$word") ) return 0 }} complete -o nospace -F _bash_{func_name} "{command}" fi """ print( template.format( func_name=func_name, command=command_name, completer=sys.argv[0], model=command_model, ) ) python-nubia-0.2.3/poetry.lock000066400000000000000000001135531434345131700163420ustar00rootroot00000000000000[[package]] name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = "*" [[package]] name = "astroid" version = "2.12.13" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} [[package]] name = "black" version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codecov" version = "2.1.12" description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] coverage = "*" requests = ">=2.7.9" [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" [package.extras] toml = ["tomli"] [[package]] name = "dill" version = "0.3.6" description = "serialize all of python" category = "dev" optional = false python-versions = ">=3.7" [package.extras] graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false python-versions = "*" [[package]] name = "filelock" version = "3.8.2" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = ">=3.6.1" [package.dependencies] importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "identify" version = "2.5.9" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "importmagic" version = "0.1.7" description = "Python Import Magic - automagically add, remove and manage imports" category = "dev" optional = false python-versions = "*" [[package]] name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] [[package]] name = "jedi" version = "0.18.2" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] parso = ">=0.8.0,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx-rtd-theme (==0.4.3)", "sphinx (==1.8.5)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jellyfish" version = "0.8.9" description = "a library for doing approximate and phonetic matching of strings." category = "main" optional = false python-versions = ">3.5" [[package]] name = "later" version = "20.10.1" description = "A toolbox for asyncio services" category = "dev" optional = false python-versions = ">3.7" [package.dependencies] async-timeout = ">=2.0.0,<5.0.0" [[package]] name = "lazy-object-proxy" version = "1.8.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false python-versions = ">=3.7" [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "main" optional = false python-versions = "*" [[package]] name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" [[package]] name = "nose" version = "1.3.7" description = "nose extends unittest to make testing easier" category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "parso" version = "0.8.3" description = "A Python Parser" category = "dev" optional = false python-versions = ">=3.6" [package.extras] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathspec" version = "0.10.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" [[package]] name = "platformdirs" version = "2.5.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] [[package]] name = "pre-commit" version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" [[package]] name = "prettytable" version = "2.5.0" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] [[package]] name = "prompt-toolkit" version = "3.0.33" description = "Library for building powerful interactive command lines in Python" category = "main" optional = false python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" [[package]] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pylint" version = "2.15.8" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] astroid = ">=2.12.13,<=2.14.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = ">=0.2" isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" version = "2.4.7" description = "Python parsing module" category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytoolconfig" version = "1.2.2" description = "Python tool configuration" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] appdirs = {version = ">=1.4.4", optional = true, markers = "extra == \"global\""} packaging = ">=21.3" tomli = {version = ">=2.0", markers = "python_version < \"3.11\""} typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] validation = ["pydantic (>=1.7.4)"] global = ["appdirs (>=1.4.4)"] gen_docs = ["pytoolconfig", "sphinx-rtd-theme (>=1.0.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx (>=4.5.0)"] doc = ["sphinx (>=4.5.0)", "tabulate (>=0.8.9)"] [[package]] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "requests" version = "2.28.1" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<3" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rope" version = "1.5.1" description = "a python refactoring library..." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] pytoolconfig = {version = ">=1.2.2", extras = ["global"]} [package.extras] dev = ["pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)", "build (>=0.7.0)"] doc = ["pytoolconfig", "sphinx (>=4.5.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx-rtd-theme (>=1.0.0)"] [[package]] name = "termcolor" version = "1.1.0" description = "ANSII Color formatting for output in terminal." category = "main" optional = false python-versions = "*" [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" [[package]] name = "tomlkit" version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" [[package]] name = "typing-inspect" version = "0.7.1" description = "Runtime inspection utilities for typing module." category = "main" optional = false python-versions = "*" [package.dependencies] mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" [[package]] name = "urllib3" version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" category = "main" optional = false python-versions = "*" [[package]] name = "wrapt" version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"] [metadata] lock-version = "1.1" python-versions = ">3.7.2,<3.10" content-hash = "27f26cd2f94ce9e56cd6577b395c641f049a0170832eddcf7ed9adf1f047477e" [metadata.files] appdirs = [] astroid = [] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] black = [] certifi = [] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [] click = [] codecov = [] colorama = [] coverage = [] dill = [] distlib = [] filelock = [] flake8 = [] identify = [] idna = [] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] importmagic = [] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jedi = [] jellyfish = [ {file = "jellyfish-0.8.9-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a3b3a31762b8c1286241ddd8a01c61d34df647d805bb5f659b8d6936612265d3"}, {file = "jellyfish-0.8.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4b2e1886f96b5721a1cbf2f2c22e8fe94af9688aec8e29945c11ab8f129a02"}, {file = "jellyfish-0.8.9-cp310-cp310-win_amd64.whl", hash = "sha256:b1fd092e60e06115da2f4ab754ba11df7740d5bfe19021c73616b456240315c8"}, {file = "jellyfish-0.8.9-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:13696b496dee907e86bc1966acaa53a85d3ac893221caa9fe020630cb49c0c4d"}, {file = "jellyfish-0.8.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dd016d3731d90231a5f1874a91b05c4fea12de1fba1e7830484b1f30d78de4c"}, {file = "jellyfish-0.8.9-cp36-cp36m-win_amd64.whl", hash = "sha256:beebadb1602ef407b94d9b31914fd7e4e260e6e62e09865276f59cff1278a7e8"}, {file = "jellyfish-0.8.9-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:77837d9156d34af8056cbf05818506549da3e585179ec8217ea32d9f5e2ad578"}, {file = "jellyfish-0.8.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2340c137a03dd3e64ed3eb5d60b7353c162bed80d7cd8bcb3c5e9053d325d724"}, {file = "jellyfish-0.8.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a9ba695c97a8c3dc0b08d44503a137a12a2e6ce3cf02d4d77f1831aa4a5f7218"}, {file = "jellyfish-0.8.9-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f218e8a004d8748f251923019d7979a7f83accc95b3a5f1d656d543b8f5b5291"}, {file = "jellyfish-0.8.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5997559270da8df1566d3a19cfa89b7585c9da5aa60010bb925247e482c73afe"}, {file = "jellyfish-0.8.9-cp38-cp38-win_amd64.whl", hash = "sha256:9965ebaecba43c709ad4b93cd1e7cb0c0c452d58a339459e0d69c4b57e81f84d"}, {file = "jellyfish-0.8.9-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:3ad030b96f7d459daa97da249ca68d74ed261367b58e9af1f02c7bded7ed8e45"}, {file = "jellyfish-0.8.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb30a07adf110d6f7d03d6bd531b1947134b1517ef45147f1329c9715ab4b9ce"}, {file = "jellyfish-0.8.9-cp39-cp39-win_amd64.whl", hash = "sha256:92d69421ed39d4036ea82d7a726300b1e016368cf5f31c3b209887390165d30e"}, {file = "jellyfish-0.8.9.tar.gz", hash = "sha256:90d25e8f5971ebbcf56f216ff5bb65d6466572b78908c88c47ab588d4ea436c2"}, ] later = [] lazy-object-proxy = [] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] nodeenv = [] nose = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] parso = [] pathspec = [] platformdirs = [] pre-commit = [] prettytable = [] prompt-toolkit = [] pycodestyle = [] pyflakes = [] pygments = [] pylint = [] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytoolconfig = [] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] requests = [] rope = [] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [] typed-ast = [] typing-extensions = [] typing-inspect = [ {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, ] urllib3 = [] virtualenv = [] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] zipp = [] python-nubia-0.2.3/pyproject.toml000066400000000000000000000027471434345131700170640ustar00rootroot00000000000000# Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # NOTE: you have to use single-quoted strings in TOML for regular expressions. # It's the equivalent of r-strings in Python. Multiline strings are treated as # verbose regular expressions by Black. Use [ ] to denote a significant space # character. [tool.black] line-length = 88 include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [tool.poetry] name = "nubia-cli" version = "0.2.3" description = "A fork of Meta's nubia, a framework for building beautiful shells." authors = ["Ahmed Soliman ", "Andreas Backx ", "Stardust Systems Dev Team"] license = "BSD" packages = [ { include = "nubia" } ] include = ["LICENSE"] [tool.poetry.dependencies] python = ">3.7.2,<3.10" jellyfish = "^0.8.9" prettytable = "^2.4.0" prompt-toolkit = "^3.0.23" Pygments = "^2.10.0" pyparsing = "^2.4.7" termcolor = "^1.1.0" typing-inspect = "^0.7.1" wcwidth = "^0.2.5" [tool.poetry.dev-dependencies] codecov = "^2.1.12" nose = "^1.3.7" pre-commit = "^2.16.0" black = "^22.6.0" importmagic = "^0.1.7" pylint = "^2.14.5" later = "^20.10.1" flake8 = "^5.0.4" jedi = "^0.18.1" rope = "^1.3.0" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" python-nubia-0.2.3/tests/000077500000000000000000000000001434345131700153005ustar00rootroot00000000000000python-nubia-0.2.3/tests/__init__.py000066400000000000000000000000001434345131700173770ustar00rootroot00000000000000python-nubia-0.2.3/tests/cmdloader_test.py000066400000000000000000000020341434345131700206420ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import unittest from nubia.internal import cmdloader from tests import empty_package, sample_package class CommandLoaderTest(unittest.TestCase): def test_load_no_packages(self): self.assertEqual([], list(cmdloader.load_commands(None))) def test_load_empty_packages(self): self.assertEqual([], list(cmdloader.load_commands(empty_package))) def test_load_sample_packages(self): loaded = list(cmdloader.load_commands(sample_package)) self.assertEqual(4, len(loaded)) from tests.sample_package import commands from tests.sample_package.subpackage import more_commands self.assertTrue(commands.example_command1 in loaded) self.assertTrue(more_commands.example_command2 in loaded) self.assertTrue(more_commands.SuperCommand in loaded) python-nubia-0.2.3/tests/commands_test.py000066400000000000000000000436611434345131700205240ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from typing import List, Optional from later.unittest import TestCase from termcolor import cprint from nubia import argument, command, deprecated from tests.util import TestShell class CommandSpecTest(TestCase): async def test_command_sync(self): @command def test_command() -> int: """ Sample Docstring """ return 22 shell = TestShell(commands=[test_command]) self.assertEqual(22, await shell.run_cli_line("test_shell test-command ")) async def test_command_name_spec1(self): @command @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: List[str]) -> int: """ Sample Docstring """ self.assertEqual(["a", "b"], arg) cprint(arg, "green") return 22 shell = TestShell(commands=[test_command]) self.assertEqual( 22, await shell.run_cli_line("test_shell test-command --arg a b") ) self.assertEqual( 22, await shell.run_interactive_line('test-command arg=["a","b"]') ) self.assertEqual( 22, await shell.run_interactive_line("test-command arg=[a, b]") ) async def test_command_name_spec2(self): """ Explicitly setting the command name with underscore, we should respect the supplied name and not auto-transform it """ @command("bleh_command") @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: List[str]) -> int: """ Sample Docstring """ self.assertEqual(["a", "b"], arg) cprint(arg, "green") return 22 shell = TestShell(commands=[test_command]) self.assertEqual( 22, await shell.run_cli_line("test_shell bleh_command --arg a b") ) self.assertEqual( 22, await shell.run_interactive_line('bleh_command arg=["a","b"]') ) self.assertEqual( 22, await shell.run_interactive_line("bleh_command arg=[a, b]") ) async def test_command_async(self): @command @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: List[str]) -> int: """ Sample Docstring """ self.assertEqual(["a", "b"], arg) cprint(arg, "green") return 22 shell = TestShell(commands=[test_command]) self.assertEqual( 22, await shell.run_cli_line("test_shell test-command --arg a b") ) self.assertEqual( 22, await shell.run_interactive_line('test-command arg=["a","b"]') ) async def test_command_aliases_spec(self): """ Testing aliases """ @command("bleh_command", aliases=["bleh"]) @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: List[str]) -> int: """ Sample Docstring """ self.assertEqual(["a", "b"], arg) cprint(arg, "green") return 22 shell = TestShell(commands=[test_command]) self.assertEqual(22, await shell.run_cli_line("test_shell bleh -i a b")) async def test_command_find_approx_spec(self): """ Testing approximate command / subcommand typing """ @command("command_first", aliases=["first"]) @argument("arg", description="argument help", aliases=["i"]) async def test_command_1(arg: int = 22) -> int: """ Sample Docstring """ cprint(arg, "green") return arg @command("command_second", aliases=["second"]) @argument("arg", description="argument help", aliases=["i"]) async def test_command_2(arg: int = 23) -> int: """ Sample Docstring """ cprint(arg, "green") return arg shell = TestShell(commands=[test_command_1, test_command_2]) # correct command name self.assertEqual(22, await shell.run_interactive_line("first")) # unique prefix command name self.assertEqual(22, await shell.run_interactive_line("f")) # unique levenshtein command name self.assertEqual(22, await shell.run_interactive_line("firts")) # unique prefix + levenshtein command name self.assertEqual(22, await shell.run_interactive_line("firs")) # non-unique prefix command name self.assertEqual(None, await shell.run_interactive_line("command")) # approximate matching only works for interactive mode, not CLI self.assertEqual(22, await shell.run_cli_line("test_shell first")) with self.assertRaises(SystemExit): await shell.run_cli_line("test_shell f") with self.assertRaises(SystemExit): await shell.run_cli_line("test_shell firts") with self.assertRaises(SystemExit): await shell.run_cli_line("test_shell firs") with self.assertRaises(SystemExit): await shell.run_cli_line("test_shell command") async def test_no_type_works_the_same(self): @command @argument("arg", positional=True) async def test_command(arg: str) -> int: """ Sample Docstring """ self.assertIsInstance(arg, str) self.assertEqual("1", arg) return 64 + int(arg) shell = TestShell(commands=[test_command]) self.assertEqual(65, await shell.run_cli_line("test_shell test-command 1")) self.assertEqual(65, await shell.run_interactive_line("test-command 1")) self.assertEqual(65, await shell.run_interactive_line('test-command "1"')) @command @argument("arg") async def test_command(arg: str) -> int: """ Sample Docstring """ self.assertIsInstance(arg, str) self.assertEqual("1", arg) return 64 + int(arg) shell = TestShell(commands=[test_command]) self.assertEqual( 65, await shell.run_cli_line("test_shell test-command --arg 1") ) self.assertEqual( 65, await shell.run_interactive_line("test-command arg=1"), ) self.assertEqual( 65, await shell.run_interactive_line('test-command arg="1"'), ) async def test_command_with_postional(self): @command @argument("arg1", positional=True) @argument("arg2", positional=True) @argument("arg3", positional=True) async def test_command(arg1: str, arg2: str, arg3: str) -> int: """ Sample Docstring """ cprint([arg1, arg2]) self.assertEqual("1", arg1) self.assertIsInstance(arg1, str) self.assertEqual("2", arg2) self.assertIsInstance(arg2, str) self.assertEqual("nubia", arg3) return 64 * int(arg1) + int(arg2) shell = TestShell(commands=[test_command]) self.assertEqual( 66, await shell.run_cli_line("test_shell test-command 1 2 nubia") ) self.assertEqual(66, await shell.run_interactive_line("test-command 1 2 nubia")) async def test_command_with_extra_spaces(self): @command @argument("arg1", positional=True) async def test_command(arg1: str) -> None: """ Sample Docstring """ self.assertEqual("1", arg1) self.assertIsInstance(arg1, str) return True shell = TestShell(commands=[test_command]) self.assertTrue(await shell.run_interactive_line("test-command 1")) self.assertTrue(await shell.run_interactive_line("test-command 1")) self.assertTrue(await shell.run_interactive_line("test-command 1")) self.assertTrue(await shell.run_interactive_line(" test-command 1")) self.assertTrue(await shell.run_interactive_line(" test-command 1")) self.assertTrue(await shell.run_interactive_line("test-command 1 ")) self.assertTrue(await shell.run_interactive_line("test-command 1 ")) self.assertTrue(await shell.run_interactive_line(" test-command 1 ")) async def test_command_with_postional_and_named_arguments(self): @command @argument("arg2", positional=True) @argument("arg3", positional=True) async def test_command(arg1: str, arg2: str, arg3: str) -> int: """ Sample Docstring """ cprint([arg1, arg2]) self.assertEqual("1", arg1) self.assertIsInstance(arg1, str) self.assertEqual("2", arg2) self.assertIsInstance(arg2, str) self.assertEqual("nubia", arg3) return 64 * int(arg1) + int(arg2) shell = TestShell(commands=[test_command]) self.assertEqual( 66, await shell.run_cli_line("test_shell test-command --arg1=1 2 nubia") ) self.assertEqual( 66, await shell.run_interactive_line("test-command arg1=1 2 nubia") ) self.assertEqual( 66, await shell.run_interactive_line("test-command arg1=1 arg2=2 nubia") ) # Fails parsing because positionals have to be at the end self.assertEqual( 1, await shell.run_interactive_line("test-command 2 nubia arg1=1") ) async def test_command_with_mutex_groups(self): @command(exclusive_arguments=["arg1", "arg2"]) @argument("arg1") @argument("arg2") async def test_command(arg1: str = "0", arg2: str = "0") -> int: """ Sample Docstring """ return 64 * int(arg1) + int(arg2) shell = TestShell(commands=[test_command]) self.assertEqual( 64, await shell.run_cli_line("test_shell test-command --arg1 1") ) self.assertEqual( 64, await shell.run_interactive_line("test-command arg1=1"), ) self.assertEqual( 2, await shell.run_cli_line("test_shell test-command --arg2 2") ) self.assertEqual( 2, await shell.run_interactive_line("test-command arg2=2"), ) with self.assertRaises(SystemExit): await shell.run_cli_line("test_shell test-command --arg1 1 --arg2 2") self.assertEqual( 66, await shell.run_interactive_line("test-command arg1=1 arg2=2"), "We are not enforsing mutex groups on interactive", ) async def test_command_with_mutex_groups_two_positionals(self): msg = "We don't supporting mutex group with required arguments" with self.assertRaises(ValueError, msg=msg): @command(exclusive_arguments=["arg1", "arg2"]) @argument("arg1", positional=True) @argument("arg2") async def test_command(arg1: str, arg2: str = "lalala") -> int: """ Sample Docstring """ return -1 await TestShell(commands=[test_command]).run_async() async def test_command_default_argument(self): """ Tests that calling a command from the CLI without all arguments specified will fall back to the default arguments set in the command definition. """ @command @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: int = 22) -> int: """ Sample Docstring """ cprint(arg, "green") return arg shell = TestShell(commands=[test_command]) self.assertEqual(22, await shell.run_cli_line("test_shell test-command")) self.assertEqual(22, await shell.run_interactive_line("test-command")) async def test_command_optional_argument(self): """ Same as above but check for make the argument optional in Python sense. """ @command @argument("arg", description="argument help", aliases=["i"]) async def test_command(arg: Optional[List[str]] = None) -> int: """ Sample Docstring """ arg = arg or ["42"] cprint(arg, "green") return sum(int(x) for x in arg) shell = TestShell(commands=[test_command]) self.assertEqual(42, await shell.run_cli_line("test_shell test-command")) self.assertEqual(42, await shell.run_interactive_line("test-command")) self.assertEqual(0, await shell.run_cli_line("test_shell test-command --arg 0")) self.assertEqual( 0, await shell.run_interactive_line("test-command arg=[0]"), ) async def test_command_one_required_one_default_argument(self): """ Tests that calling a command from the CLI without all arguments specified will fall back to the default arguments set in the command definition. """ @command("bleh_command") @argument("arg1", description="argument help", aliases=["i1"]) @argument("arg2", description="argument 2 help", aliases=["i2"]) async def test_command(arg1: int, arg2: int = 1) -> int: """ Sample Docstring """ cprint(arg1, "green") return arg1 + arg2 shell = TestShell(commands=[test_command]) self.assertEqual( 22, await shell.run_cli_line("test_shell bleh_command --arg1=21") ) self.assertEqual( 22, await shell.run_interactive_line("bleh_command arg1=21"), ) async def test_command_for_blacklist_plugin_allowed(self): @command("allowed") async def test_command(): """ Sample Docstring """ cprint("Command Executed as required", "green") return 42 shell = TestShell(commands=[test_command]) self.assertEqual(42, await shell.run_cli_line("test_shell allowed")) self.assertEqual(42, await shell.run_interactive_line("allowed")) async def test_command_for_blacklist_plugin_blacklisted(self): @command("blocked") async def test_command(): """ Sample Docstring """ cprint("Command executed, but should be blocked", "red") return 3 shell = TestShell(commands=[test_command]) self.assertEqual(1, await shell.run_cli_line("test_shell blocked")) self.assertEqual(1, await shell.run_interactive_line("blocked")) async def test_command_with_negative_ints(self): @command("minus_command") @argument("arg1", type=int) async def test_command(arg1): """ Sample Docstring """ self.assertEqual(type(5), type(arg1)) return 42 if arg1 == -1 else -1 shell = TestShell(commands=[test_command]) # Cli run self.assertEqual( 42, await shell.run_cli_line("test_shell minus_command --arg1=-1") ) # Interactive self.assertEqual(42, await shell.run_interactive_line("minus_command arg1=-1")) async def test_command_with_negative_floats(self): @command("minus_command") @argument("arg1", type=float) async def test_command(arg1): """ Sample Docstring """ self.assertEqual(type(5.0), type(arg1)) return 42 if arg1 == -1.0 else 55 shell = TestShell(commands=[test_command]) # Cli run self.assertEqual( 42, await shell.run_cli_line("test_shell minus_command --arg1=-1") ) self.assertEqual( 42, await shell.run_cli_line("test_shell minus_command --arg1=-1.0") ) # Interactive self.assertEqual(42, await shell.run_interactive_line("minus_command arg1=-1")) self.assertEqual( 42, await shell.run_interactive_line("minus_command arg1=-1.0") ) async def test_command_deprecation(self): @deprecated(superseded_by="new-command") @command def old_command() -> int: """ Sample Docstring """ cprint("This command is deprecated", "yellow") return new_command() @command def new_command() -> int: """ Sample Docstring """ cprint("This is the future", "green") return 42 shell = TestShell(commands=[old_command, new_command]) self.assertEqual(42, await shell.run_cli_line("test_shell old-command")) self.assertEqual(42, await shell.run_interactive_line("old-command")) self.assertEqual(42, await shell.run_cli_line("test_shell new-command")) self.assertEqual(42, await shell.run_interactive_line("new-command")) async def test_type_lifting(self): @command @argument("args") async def test_command(args: List[str]) -> str: """ Sample Docstring """ return "|".join(args) shell = TestShell(commands=[test_command]) # CLI self.assertEqual( "a", await shell.run_cli_line("test_shell test-command --args a") ) self.assertEqual( "a|b", await shell.run_cli_line("test_shell test-command --args a b") ) # Interactive self.assertEqual("a", await shell.run_interactive_line('test-command args="a"')) self.assertEqual( "a", await shell.run_interactive_line('test-command args=["a"]') ) self.assertEqual( "a|b", await shell.run_interactive_line('test-command args=["a", "b"]') ) python-nubia-0.2.3/tests/empty_package/000077500000000000000000000000001434345131700201115ustar00rootroot00000000000000python-nubia-0.2.3/tests/empty_package/__init__.py000066400000000000000000000000001434345131700222100ustar00rootroot00000000000000python-nubia-0.2.3/tests/helpers_test.py000066400000000000000000000062011434345131700203520ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import unittest from typing import Dict, List, Optional, Union from nubia.internal.helpers import ( catchall, find_approx, function_to_str, suggestions_msg, ) from nubia.internal.typing.inspect import is_optional_type class HelpersTest(unittest.TestCase): def test_function_to_str(self): def foo(arg1, arg2, *args, **kwargs): pass def test(expected, with_module, with_args): self.assertEqual(function_to_str(foo, with_module, with_args), expected) test("foo", False, False) test("tests.helpers_test.foo", True, False) test("foo(arg1, arg2, *args, **kwargs)", False, True) test("tests.helpers_test.foo(arg1, arg2, *args, **kwargs)", True, True) def test_catchall(self): def raise_generic_error(): raise RuntimeError() def raise_keyboard_interrupt(): raise KeyboardInterrupt() def raise_sysexit(): raise SystemExit() # expected catch all errors except keyboard, sysexit catchall(raise_generic_error) self.assertRaises(KeyboardInterrupt, catchall, raise_keyboard_interrupt) self.assertRaises(SystemExit, catchall, raise_sysexit) def test_find_approx(self): commands_map = ["maintenance", "malloc", "move", "list"] # check levenshtein approximation self.assertEqual(find_approx("maintenanec", commands_map), ["maintenance"]) self.assertEqual(find_approx("ls", commands_map), ["list"]) # check prefix matching with single result self.assertEqual(find_approx("mal", commands_map), ["malloc"]) self.assertEqual(find_approx("maint", commands_map), ["maintenance"]) # check prefix matching and levenshtein don't generate duplicate suggestions self.assertEqual(find_approx("lis", commands_map), ["list"]) # check prefix matching with more than one result - should return none self.assertEqual(find_approx("ma", commands_map), ["maintenance", "malloc"]) self.assertEqual( find_approx("m", commands_map), ["maintenance", "malloc", "move"] ) # check no results self.assertEqual(find_approx("a", commands_map), []) def test_is_optional(self): self.assertFalse(is_optional_type(List[str])) self.assertFalse(is_optional_type(Dict[str, int])) self.assertFalse(is_optional_type(Union[str, int])) self.assertTrue(is_optional_type(Optional[str])) self.assertTrue(is_optional_type(Union[str, None])) def test_suggestions_msg(self): suggestions = [] self.assertEqual(suggestions_msg(suggestions), "") suggestions = ["one", "two"] self.assertEqual(suggestions_msg(suggestions), ", Did you mean one or two?") suggestions = ["one", "two", "three"] self.assertEqual( suggestions_msg(suggestions), ", Did you mean one, two or three?" ) python-nubia-0.2.3/tests/inspection_test.py000066400000000000000000000041451434345131700210700ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import unittest from nubia import command from nubia.internal.typing import inspect_object class InspectionTest(unittest.TestCase): def test_inspect_function1(self): @command def my_function(arg1: str, argument_2: int): """HelpMessage""" pass data = inspect_object(my_function) cmd = data.command args = data.arguments self.assertEqual("my-function", cmd.name) self.assertEqual("HelpMessage", cmd.help) self.assertEqual(2, len(args)) self.assertTrue("arg1" in args.keys()) self.assertTrue("argument-2" in args.keys()) def test_inspect_class(self): @command class SuperCommand: """SuperHelp""" @command def my_function(self, arg1: str, argument_2: int): """HelpMessage""" pass data = inspect_object(SuperCommand) cmd = data.command args = data.arguments self.assertEqual("super-command", cmd.name) self.assertEqual("SuperHelp", cmd.help) self.assertEqual(0, len(args.keys())) self.assertEqual(1, len(data.subcommands)) subcmd_attr, subcmd_insp = data.subcommands[0] self.assertEqual("my_function", subcmd_attr) subcmd = subcmd_insp.command self.assertEqual("my-function", subcmd.name) self.assertEqual("HelpMessage", subcmd.help) subargs = subcmd_insp.arguments self.assertEqual(2, len(subargs)) self.assertTrue("arg1" in subargs.keys()) self.assertTrue("argument-2" in subargs.keys()) def test_inspect_no_docstring(self): @command class SuperCommand: """SuperHelp""" @command def my_function(self, arg1: str): pass data = inspect_object(SuperCommand) self.assertListEqual([], data.subcommands) python-nubia-0.2.3/tests/read_stdin_test.py000066400000000000000000000021431434345131700210250ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import os import sys import tempfile from later.unittest import TestCase from nubia import argument, command from tests.util import TestShell class ReadStdinTest(TestCase): async def test_read_from_stdin(self): @command @argument("arg") def test_command(arg: str) -> int: """ Sample Docstring """ self.assertEqual("test_arg", arg) return 22 command_file = tempfile.NamedTemporaryFile( mode="w+", prefix="test_read_from_stdin", delete=True ) command_file.write("test-command arg=test_arg") command_file.flush() os.lseek(command_file.fileno(), 0, os.SEEK_SET) os.dup2(command_file.fileno(), sys.stdin.fileno()) shell = TestShell(commands=[test_command]) self.assertEqual(22, await shell.run_async(cli_args=["", "connect"])) python-nubia-0.2.3/tests/sample_package/000077500000000000000000000000001434345131700202345ustar00rootroot00000000000000python-nubia-0.2.3/tests/sample_package/__init__.py000066400000000000000000000000001434345131700223330ustar00rootroot00000000000000python-nubia-0.2.3/tests/sample_package/commands.py000066400000000000000000000007631434345131700224150ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia import command @command def example_command1(): """ An example command for testing purposes """ return None @command async def example_async_command1(): """ An example command for testing purposes async """ return None python-nubia-0.2.3/tests/sample_package/subpackage/000077500000000000000000000000001434345131700223415ustar00rootroot00000000000000python-nubia-0.2.3/tests/sample_package/subpackage/__init__.py000066400000000000000000000000001434345131700244400ustar00rootroot00000000000000python-nubia-0.2.3/tests/sample_package/subpackage/more_commands.py000066400000000000000000000012461434345131700255410ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia import command @command def example_command2(): """ An example command for testing purposes """ return None @command class SuperCommand: """ Super-Command Docs """ @command def sub_command(self): """ Sub-Command Docs """ return None @command async def sub_command_async(self): """ Sub-Command Docs Async """ return None python-nubia-0.2.3/tests/supercommands_test.py000066400000000000000000000060631434345131700215760ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import unittest from later.unittest import TestCase from nubia import command from tests.util import TestShell class SuperCommandSpecTest(TestCase): async def test_super_basics(self): this = self @command class SuperCommand: "SuperHelp" @command async def sub_command(self, arg1: str, arg2: int): "SubHelp" this.assertEqual(arg1, "giza") this.assertEqual(arg2, 22) return 45 @command async def another_command(self, arg1: str): "AnotherHelp" return 22 shell = TestShell(commands=[SuperCommand]) self.assertEqual( 45, await shell.run_cli_line( "test_shell super-command sub-command --arg1=giza --arg2=22" ), ) self.assertEqual( 22, await shell.run_cli_line( "test_shell super-command another-command --arg1=giza" ), ) async def test_super_common_arguments(self): this = self @command class SuperCommand: "SuperHelp" def __init__(self, shared: int = 10) -> None: self.shared = shared @command async def sub_command(self, arg1: str, arg2: int): "SubHelp" this.assertEqual(self.shared, 15) this.assertEqual(arg1, "giza") this.assertEqual(arg2, 22) return 45 shell = TestShell(commands=[SuperCommand]) self.assertEqual( 45, await shell.run_cli_line( "test_shell super-command --shared=15 " "sub-command --arg1=giza --arg2=22" ), ) self.assertEqual( 45, await shell.run_cli_line( "test_shell super-command sub-command " "--arg1=giza --arg2=22 --shared=15" ), ) async def test_super_no_docstring(self): @command class SuperCommand: "SuperHelp" @command async def sub_command(self, arg1: str): return f"Hi {arg1}" shell = TestShell(commands=[SuperCommand]) with self.assertRaises(SystemExit): await shell.run_cli_line( "test_shell super-command sub-command --arg1=human" ) async def test_sync_sub_command(self): @command class SuperCommand: "SuperHelp" @command def sub_command(self): "SubHelp" return 45 shell = TestShell(commands=[SuperCommand]) self.assertEqual( 45, await shell.run_cli_line("test_shell super-command sub-command"), ) python-nubia-0.2.3/tests/typing-py3-style.py000066400000000000000000000047271434345131700210450ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # """ ############################################################################ # Python 3 only tests. This is not expected to work on python 2. # Any test that should work on both python 2 and 3 should go to tests.py ############################################################################ """ import typing import unittest from nubia.internal.typing import argument, inspect_object class DecoratorTest(unittest.TestCase): def test_equality_decorated(self): @argument("arg1", description="arg1 desc") @argument("arg2", description="arg2 desc") def foo(arg1: typing.Any, arg2: str) -> typing.Tuple[str, str]: return (arg1, arg2) @argument("arg1", type=typing.Any, description="arg1 desc") @argument("arg2", type=str, description="arg2 desc") def bar(arg1, arg2): return (arg1, arg2) self.assertEqual(inspect_object(foo), inspect_object(bar)) def test_inequality_no_decorator(self): def foo(arg1: str, arg2: str) -> typing.Tuple[str, str]: return (arg1, arg2) def bar(arg1, arg2): return (arg1, arg2) self.assertNotEqual(inspect_object(foo), inspect_object(bar)) def test_inequality_decorated(self): def foo(arg1: str, arg2: str) -> typing.Tuple[str, str]: return (arg1, arg2) @argument("arg1", type=int) @argument("arg2", type=int) def bar(arg1, arg2): return (arg1, arg2) self.assertNotEqual(inspect_object(foo), inspect_object(bar)) def test_type_conflict(self): # specifiying arg as str in both the decorator and in the type # annotation is redundant but should be fine @argument("arg", type=str) def foo(arg: str) -> str: return arg try: # arg is being specified as str by the decorator but as typing.Any # by the type annotation. A TypeError should be raised @argument("arg", type=str) def bar(arg: typing.Any) -> str: return arg self.fail( "foo declaration should fail with TypeError as it " "declares arg as both str and typing.Any" ) except TypeError: pass python-nubia-0.2.3/tests/typing_test.py000066400000000000000000000550661434345131700202370ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import argparse import inspect import typing import unittest from io import StringIO from nubia.internal.typing import argument, command from nubia.internal.typing.argparse import add_command, find_command from nubia.internal.typing.builder import build_value class ParseError(Exception): pass class ContainedParser(argparse.ArgumentParser): """ Parser that gives options that avoid using sys.stdout, sys.stderr and raising SystemExit """ def help(self): return self._print_to_buffer(self.print_help) def usage(self): return self._print_to_buffer(self.print_usage) def _print_to_buffer(self, print_function): s = StringIO() print_function(s) return s.getvalue() def error(self, message): raise ParseError(message) class SimpleValuesBuilderTest(unittest.TestCase): def test_build_string(self): value = build_value("some string", str, False) self.assertEqual(value, "some string") value = build_value('"some string"', str, True) self.assertEqual(value, "some string") def test_build_int(self): value = build_value("1", int, False) self.assertEqual(value, 1) value = build_value("1", int, True) self.assertEqual(value, 1) def test_build_custom_type(self): def parser(string): return string.split("#") value = build_value("special#string", parser, False) self.assertEqual(value, ["special", "string"]) value = build_value('"special#string"', parser, True) self.assertEqual(value, ["special", "string"]) def test_build_tuple(self): value = build_value("foo bar,1,0.5", typing.Tuple[str, int, float], False) self.assertEqual(value, ("foo bar", 1, 0.5)) value = build_value('("foo bar",1,0.5)', typing.Tuple[str, int, float], True) self.assertEqual(value, ("foo bar", 1, 0.5)) def test_build_tuple_partially_typed(self): value = build_value( "foo bar,1,0.5", typing.Tuple[str, typing.Any, float], False ) self.assertEqual(value, ("foo bar", "1", 0.5)) value = build_value( '("foo bar",1,0.5)', typing.Tuple[str, typing.Any, float], True ) self.assertEqual(value, (str("foo bar"), 1, 0.5)) def test_build_tuple_untyped(self): value = build_value("foo bar,1,0.5", typing.Tuple, False) self.assertEqual(value, ("foo bar", "1", "0.5")) value = build_value('("foo bar",1,0.5)', typing.Tuple, True) self.assertEqual(value, (str("foo bar"), 1, 0.5)) def test_build_tuple_single_element(self): value = build_value("foo bar", typing.Tuple[str], False) self.assertEqual(value, ("foo bar",)) value = build_value('("foo bar",)', typing.Tuple[str], True) self.assertEqual(value, (str("foo bar"),)) def test_build_typed_dict(self): value = build_value("a:1;b:2", typing.Mapping[str, int], False) self.assertEqual(value, {"a": 1, "b": 2}) value = build_value( '{"a": "1", "b": 2, "c": 3.2}', typing.Mapping[str, int], True ) self.assertEqual(value, {"a": 1, "b": 2, "c": 3}) def test_build_typed_dict_mixed(self): value = build_value("a=1;b=2", typing.Mapping[str, int], False) self.assertEqual(value, {"a": 1, "b": 2}) value = build_value("a:1;b=2", typing.Mapping[str, int], False) self.assertEqual(value, {"a": 1, "b": 2}) def test_build_typed_dict_with_list(self): value = build_value("a=1,2,3;b=2", typing.Mapping[str, str], False) self.assertEqual(value, {"a": "1,2,3", "b": "2"}) value = build_value("a=1,2,3;b=2", typing.Mapping[str, typing.List[int]], False) self.assertEqual(value, {"a": [1, 2, 3], "b": [2]}) def test_build_partially_typed_dict(self): value = build_value("a:1;b:2", typing.Mapping[typing.Any, int], False) self.assertEqual(value, {"a": 1, "b": 2}) value = build_value( '{"a": "1", "b": 2, 0: 3}', typing.Mapping[typing.Any, int], True ) self.assertEqual(value, {"a": 1, "b": 2, 0: 3}) def test_build_untyped_dict(self): value = build_value("a:1;b:2", typing.Mapping, False) self.assertEqual(value, {"a": "1", "b": "2"}) value = build_value('{"a": 1, "b": 2.5}', typing.Mapping, True) self.assertEqual(value, {"a": 1, "b": 2.5}) def test_build_typed_list(self): value = build_value("1,2,3", typing.List[int], False) self.assertEqual(value, [1, 2, 3]) value = build_value("hello,world,test", typing.List[str], False) self.assertEqual(value, ["hello", "world", "test"]) value = build_value("hello", typing.List[str], False) self.assertEqual(value, ["hello"]) value = build_value('["1",2,3.2]', typing.List[int], True) self.assertEqual(value, [1, 2, 3]) def test_build_untyped_list(self): value = build_value("1,2,3", typing.List, False) self.assertEqual(value, ["1", "2", "3"]) value = build_value('["1",2,3.5]', typing.List, True) self.assertEqual(value, ["1", 2, 3.5]) def test_build_any_typed_list(self): value = build_value("1,2,3", typing.List[typing.Any], False) self.assertEqual(value, ["1", "2", "3"]) value = build_value('["1",2,3.5]', typing.List[typing.Any], True) self.assertEqual(value, ["1", 2, 3.5]) def test_build_whitespaces(self): value = build_value(" a : 1 ; b : 2 ", typing.Mapping[str, int], False) self.assertEqual(value, {"a": 1, "b": 2}) value = build_value('{ "a" : 1 , "b" : 2 }', typing.Mapping[str, int], True) self.assertEqual(value, {"a": 1, "b": 2}) value = build_value(" 1 , 2 , 3 ", typing.List[int], False) self.assertEqual(value, [1, 2, 3]) value = build_value("[ 1 , 2 , 3 ]", typing.List[int], True) self.assertEqual(value, [1, 2, 3]) value = build_value(" 1 , 2 , 3 ", typing.Tuple[int, int, int], False) self.assertEqual(value, (1, 2, 3)) value = build_value("( 1 , 2 , 3 )", typing.Tuple[int, int, int], True) self.assertEqual(value, (1, 2, 3)) def test_build_with_casting(self): value = build_value("a:1;b:2;c:3", typing.Mapping[str, float]) self.assertEqual(value, {"a": 1.0, "b": 2.0, "c": 3.0}) value = build_value("a:1;b:2;c:3", typing.Mapping[str, str]) self.assertEqual(value, {"a": "1", "b": "2", "c": "3"}) self.assertRaises( ValueError, build_value, "a:1;b:2;c:3", typing.Mapping[int, int] ) def test_build_nested_structures(self): inpt = """{ "a": 1, "b": { "c": [2, 3, 4, [5, 6]] } }""" expected = {"a": 1, "b": {"c": [2, 3, 4, [5, 6]]}} expected_type = typing.Any self.assertEqual(build_value(inpt, expected_type, True), expected) inpt = """{ "a": [ [1, 2], [3, 4] ], "b": [ [10, 20, 30], [40] ] }""" expected = {"a": [[1, 2], [3, 4]], "b": [[10, 20, 30], [40]]} # dict of str => list of list of ints expected_type = typing.Mapping[str, typing.List[typing.List[int]]] self.assertEqual(build_value(inpt, expected_type, True), expected) def test_build_tuple_error(self): # too many arguments self.assertRaises( ValueError, build_value, "foo bar,1,0.5,extra!", typing.Tuple[str, int, float], False, ) self.assertRaises( ValueError, build_value, '("foo bar", 1, 0.5, "extra!")', typing.Tuple[str, int, float], True, ) # too few arguments self.assertRaises( ValueError, build_value, "foo bar", typing.Tuple[str, int, float], False ) self.assertRaises( ValueError, build_value, '("foo bar",)', typing.Tuple[str, int, float], True ) class ArgparseExtensionTest(unittest.TestCase): def test_no_decorator_simple(self): def foo(): return "bar" def foo2(arg1, arg2): return (arg1, arg2) self._test(foo, "foo".split(), "bar") self._test( foo, "foo --invalid arg".split(), ParseError("unrecognized arguments: --invalid arg"), ) self._test(foo2, "foo2 --arg1=abc --arg2=123".split(), ("abc", "123")) self._test(foo2, "foo2 --arg1 abc --arg2 123".split(), ("abc", "123")) def test_no_decorator_defaults(self): def foo(arg1="bar"): return arg1 def foo2(arg1=True): return arg1 def foo3(arg1=False): return arg1 self._test(foo, "foo".split(), "bar") self._test(foo, "foo --arg1 lol".split(), "lol") # boolean args are exposed as flags that works as on/off switches # if the argument default is True, the flag works as an "off" switch self._test(foo2, "foo2".split(), True) self._test(foo2, "foo2 --arg1".split(), False) # if the argument default is False, the flag works as an "on" switch self._test(foo3, "foo3".split(), False) self._test(foo3, "foo3 --arg1".split(), True) def test_argument_decorated_simple(self): @argument("arg1") @argument("arg2") def foo(arg1, arg2): return "{} {}".format(arg1, arg2) self._test(foo, "foo --arg1 Hello --arg2 World".split(), "Hello World") def test_argument_decorated_different_name(self): @argument("arg1", name="banana") @argument("arg2", name="apple") def foo(arg1, arg2): return "{} {}".format(arg1, arg2) # arg2 is not decorated @argument("arg1", name="banana") def foo2(arg1, arg2): return "{} {}".format(arg1, arg2) # arg2 is decorated but pretty much useless in this form @argument("arg1", name="banana") @argument("arg2") def foo3(arg1, arg2): return "{} {}".format(arg1, arg2) self._test(foo, "foo --banana Hello --apple World".split(), "Hello World") self._test(foo2, "foo2 --banana Hello --arg2 World".split(), "Hello World") self._test(foo3, "foo3 --banana Hello --arg2 World".split(), "Hello World") self._test(foo, "foo --arg1 Hello --apple World".split(), ParseError) def test_argument_decorated_aliases(self): @argument("arg", aliases=["banana", "apple", "b", "a"]) def foo(arg): return arg self._test(foo, "foo --arg bar".split(), "bar") self._test(foo, "foo --banana bar".split(), "bar") self._test(foo, "foo --apple bar".split(), "bar") self._test(foo, "foo -b bar".split(), "bar") self._test(foo, "foo -a bar".split(), "bar") def test_argument_decorated_kwargs(self): @argument("arg", type=int, description="arg help") @argument("extra_arg", type=int, description="extra") def foo(arg, **kwargs): return (arg, kwargs) self._test(foo, "foo --arg 6".split(), (6, {"extra_arg": None})) self._test(foo, "foo --extra-arg 15".split(), ParseError) self._test(foo, "foo --arg 14 --another-extra-arg 15".split(), ParseError) self._test(foo, "foo --arg 3 --extra-arg 15".split(), (3, {"extra_arg": 15})) def test_argument_decorated_naming_conventions(self): @argument("arg_1", aliases=["_argument__1"]) @argument("arg_2", name="_argument___2") def __foo__bar__(arg_1, arg_2): return "{} {}".format(arg_1, arg_2) self._test(__foo__bar__, "foo-bar --arg-1 x --argument-2 y".split(), "x y") self._test(__foo__bar__, "foo-bar --argument-1 x --argument-2 y".split(), "x y") def test_argument_dict_list_type_lifting(self): @argument("arg_1", type=typing.Mapping[str, int]) @argument("arg_2", type=typing.List[int]) def __foo__bar__(arg_1, arg_2): return (arg_1, arg_2) self._test(__foo__bar__, "foo-bar --arg-1 x --arg-2 y".split(), ParseError) self._test(__foo__bar__, "foo-bar --arg-1 1 --arg-2 2".split(), ParseError) self._test( __foo__bar__, "foo-bar --arg-1 allData=1 --arg-2 2".split(), ({"allData": 1}, [2]), ) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data:2 --arg-2 2".split(), ({"all": 1, "nothing-data": 2}, [2]), ) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data=2 --arg-2 2".split(), ({"all": 1, "nothing-data": 2}, [2]), ) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data=2 --arg-2 2 3".split(), ({"all": 1, "nothing-data": 2}, [2, 3]), ) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data=2 --arg-2 2 3".split(), ({"all": 1, "nothing-data": 2}, [2, 3]), ) def test_argument_list_in_dict_type_lifting(self): @argument("arg_1", type=typing.Mapping[str, typing.List[int]]) def __foo__bar__(arg_1): return arg_1 self._test(__foo__bar__, "foo-bar --arg-1 x".split(), ParseError) self._test(__foo__bar__, "foo-bar --arg-1 allData=1".split(), {"allData": [1]}) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data:2".split(), {"all": [1], "nothing-data": [2]}, ) self._test( __foo__bar__, "foo-bar --arg-1 all=1,2,3;nothing-data=2".split(), {"all": [1, 2, 3], "nothing-data": [2]}, ) self._test( __foo__bar__, "foo-bar --arg-1 all=1;nothing-data=2,2,3".split(), {"all": [1], "nothing-data": [2, 2, 3]}, ) def test_argument_decorated_unknown_arg(self): with self.assertRaises(NameError): @argument("arg1", description="arg1 description") @argument("bar", description="this arg doesnt exist!") def foo(arg1, arg2): pass def test_kwargs(self): try: @argument("arg1", description="this exists!") def foo(arg1, **kwargs): pass except Exception as e: self.fail("Should not have thrown: {}".format(e)) def test_kwargs_with_arguments(self): try: @argument("arg1", description="this exists!") @argument("arg2", description="this is in kwargs!") def foo(arg1, **kwargs): pass except Exception as e: self.fail("Should not have thrown: {}".format(e)) def test_command_decorator_presence(self): def foo(): return "bar" self._test(foo, ["foo"], "bar") self._test(command(foo), ["foo"], "bar") self._test(command()(foo), ["foo"], "bar") def test_command_exclusive_args_simple(self): @command(exclusive_arguments=["arg1", "arg2"]) def foo(arg1="", arg2="", arg3=""): return ",".join(str(arg) for arg in (arg1, arg2, arg3)) self._test(foo, "foo --arg1=bar".split(), "bar,,") self._test(foo, "foo --arg2=bar".split(), ",bar,") self._test(foo, "foo --arg3=bar".split(), ",,bar") self._test(foo, "foo --arg1=bar --arg3=bar".split(), "bar,,bar") self._test(foo, "foo --arg2=bar --arg3=bar".split(), ",bar,bar") self._test(foo, "foo --arg1=bar --arg2=bar".split(), ParseError) self._test(foo, "foo --arg1=bar --arg2=bar --arg3=bar".split(), ParseError) def test_command_exclusive_args_array(self): @command(exclusive_arguments=[["arg1", "arg2"], ["arg3", "arg4"]]) def foo(arg1="", arg2="", arg3="", arg4=""): return ",".join(str(arg) for arg in (arg1, arg2, arg3, arg4)) self._test(foo, "foo --arg1=bar".split(), "bar,,,") self._test(foo, "foo --arg2=bar".split(), ",bar,,") self._test(foo, "foo --arg3=bar".split(), ",,bar,") self._test(foo, "foo --arg4=bar".split(), ",,,bar") self._test(foo, "foo --arg1=bar --arg3=bar".split(), "bar,,bar,") self._test(foo, "foo --arg1=bar --arg4=bar".split(), "bar,,,bar") self._test(foo, "foo --arg2=bar --arg3=bar".split(), ",bar,bar,") self._test(foo, "foo --arg2=bar --arg4=bar".split(), ",bar,,bar") self._test(foo, "foo --arg1=bar --arg2=bar".split(), ParseError) self._test(foo, "foo --arg3=bar --arg4=bar".split(), ParseError) self._test( foo, "foo --arg1=bar --arg2=bar --arg3=bar --arg4=bar".split(), ParseError ) def test_command_repeated_exclusive_args(self): with self.assertRaises(ValueError): # arg1 is present in two exclusive groups @command(exclusive_arguments=[["arg1", "arg2"], ["arg1", "arg3"]]) def foo(arg1="", arg2="", arg3=""): pass def test_command_unknown_exclusive_args(self): with self.assertRaises(NameError): # arg bar doesnt exist @command(exclusive_arguments=[["arg1", "bar"]]) def foo(arg1="", arg2="", arg3=""): pass def test_duplicate_argument_decorator(self): with self.assertRaises(ValueError): # two refs to the same arg @command @argument("arg", name="arg1") @argument("arg", name="arg2") def foo(arg=1): pass def test_positional_arg(self): @argument("arg", positional=True) def foo(arg): return arg self._test(foo, "foo lalala", "lalala") def test_positional_arg_with_default(self): @argument("arg1", positional=True) @argument("arg2") def foo(arg1, arg2="default_arg1"): return "{},{}".format(arg1, arg2) self._test(foo, "foo lalala", "lalala,default_arg1") self._test(foo, "foo lalala --arg2 bububu", "lalala,bububu") self._test(foo, "foo --arg2 bububu lalala", "lalala,bububu") def test_only_single_value_allowed_for_positional(self): @argument("arg1", positional=True) def foo(arg1): pass self._test(foo, "foo lalala bububu", ParseError) def test_missing_positional(self): @argument("arg", positional=True) def foo(arg): pass self._test(foo, "foo", ParseError) def test_multiple_positionals(self): @argument("arg1", positional=True) @argument("arg2", positional=True) @argument("arg3") def foo(arg1, arg2, arg3="default"): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v", "arg1v,arg2v,default") self._test(foo, "foo arg1v arg2v --arg3 arg3v", "arg1v,arg2v,arg3v") self._test(foo, "foo arg1v --arg3 arg3v arg2v", "arg1v,arg2v,arg3v") self._test(foo, "foo --arg3 arg3v arg1v arg2v", "arg1v,arg2v,arg3v") def test_multiple_positionals_not_relates_to_decorator(self): # just all permutations of three decorators @argument("arg1", positional=True) @argument("arg2", positional=True) @argument("arg3", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") @argument("arg1", positional=True) @argument("arg3", positional=True) @argument("arg2", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") @argument("arg2", positional=True) @argument("arg1", positional=True) @argument("arg3", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") @argument("arg2", positional=True) @argument("arg3", positional=True) @argument("arg1", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") @argument("arg3", positional=True) @argument("arg1", positional=True) @argument("arg2", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") @argument("arg3", positional=True) @argument("arg2", positional=True) @argument("arg1", positional=True) def foo(arg1, arg2, arg3): return ",".join([arg1, arg2, arg3]) self._test(foo, "foo arg1v arg2v arg3v", "arg1v,arg2v,arg3v") def test_positional_with_default(self): msg = ( "We explicitly do not support positional " "with default because it is confusing" ) with self.assertRaises(ValueError, msg=msg): @command @argument("arg", positional=True) def foo(arg="default"): return arg # validation happens on building parser time so let's build one parser = ContainedParser() add_command(parser, foo) def test_positional_with_aliases(self): msg = "Aliases for positional not yet supported" with self.assertRaises(ValueError, msg=msg): @command @argument("arg", positional=True, aliases=["a"]) def foo(arg="default"): return arg # validation happens on building parser time so let's build one parser = ContainedParser() add_command(parser, foo) def _test(self, command_function, arguments, expected_result): if isinstance(arguments, str): arguments = arguments.split() parser = ContainedParser() add_command(parser, command_function) try: parsed = parser.parse_args(args=arguments) except Exception as e: if inspect.isclass(expected_result): self.assertIsInstance(e, expected_result) elif isinstance(expected_result, ParseError): self.assertEqual(str(e), str(expected_result)) else: raise else: command_function = find_command(parser, parsed, True) self.assertEqual(command_function(), expected_result) python-nubia-0.2.3/tests/util.py000066400000000000000000000026531434345131700166350ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # from nubia import Nubia, PluginInterface from nubia.internal.blackcmd import CommandBlacklist from nubia.internal.cmdbase import AutoCommand, Command class TestPlugin(PluginInterface): def __init__(self, commands): self._commands = commands def get_commands(self): return [c if isinstance(c, Command) else AutoCommand(c) for c in self._commands] def getBlacklistPlugin(self): commandBlacklist = CommandBlacklist() commandBlacklist.add_blocked_command("blocked") return commandBlacklist class TestShell(Nubia): def __init__(self, commands, name="test_shell"): super(TestShell, self).__init__(name, plugin=TestPlugin(commands), testing=True) async def run_cli_line(self, raw_line): cli_args_list = raw_line.split() args = await self._pre_run(cli_args_list) return await self.run_cli(args) async def run_interactive_line(self, raw_line, cli_args=None): cli_args = cli_args or "test_shell connect" cli_args_list = cli_args.split() args = await self._pre_run(cli_args_list) io_loop = await self._create_interactive_io_loop(args) return await io_loop.parse_and_evaluate(raw_line)