httpie-0.9.8/0000755000000000000000000000000013022157774011533 5ustar rootroothttpie-0.9.8/pytest.ini0000644000000000000000000000005013022157774013557 0ustar rootroot[pytest] norecursedirs = tests/fixtures httpie-0.9.8/.editorconfig0000644000000000000000000000036513022157774014214 0ustar rootroot# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_size = 2 [Makefile] indent_style = tab indent_size = 8 httpie-0.9.8/.gitignore0000644000000000000000000000016213022157774013522 0ustar rootroot.DS_Store .idea/ __pycache__/ dist/ httpie.egg-info/ build/ *.egg-info .cache/ .tox .coverage *.pyc *.egg htmlcov httpie-0.9.8/README.rst0000644000000000000000000012617513022157774013236 0ustar rootrootHTTPie: a CLI, cURL-like tool for humans ######################################## HTTPie (pronounced *aitch-tee-tee-pie*) is a command line HTTP client. Its goal is to make CLI interaction with web services as human-friendly as possible. It provides a simple ``http`` command that allows for sending arbitrary HTTP requests using a simple and natural syntax, and displays colorized output. HTTPie can be used for testing, debugging, and generally interacting with HTTP servers. .. class:: no-web .. image:: https://raw.githubusercontent.com/jkbrzt/httpie/master/httpie.png :alt: HTTPie compared to cURL :width: 100% :align: center .. class:: no-web no-pdf |pypi| |unix_build| |windows_build| |coverage| |gitter| .. contents:: .. section-numbering:: .. raw:: pdf PageBreak oneColumn Main features ============= * Expressive and intuitive syntax * Formatted and colorized terminal output * Built-in JSON support * Forms and file uploads * HTTPS, proxies, and authentication * Arbitrary request data * Custom headers * Persistent sessions * Wget-like downloads * Python 2.6, 2.7 and 3.x support * Linux, Mac OS X and Windows support * Plugins * Documentation * Test coverage Installation ============ macOS ----- On macOS, HTTPie can be installed via `Homebrew `_ (recommended): .. code-block:: bash $ brew install httpie A MacPorts *port* is also available: .. code-block:: bash $ port install httpie Linux ----- Most Linux distributions provide a package that can be installed using the system package manager, e.g.: .. code-block:: bash # Debian-based distributions such as Ubuntu: $ apt-get install httpie # RPM-based distributions: $ yum install httpie # Arch Linux $ pacman -S httpie Windows, etc. ------------- A universal installation method (that works on Windows, Mac OS X, Linux, …, and always provides the latest version) is to use `pip`_: .. code-block:: bash # Make sure we have an up-to-date version of pip and setuptools: $ pip install --upgrade pip setuptools $ pip install --upgrade httpie (If ``pip`` installation fails for some reason, you can try ``easy_install httpie`` as a fallback.) Development version ------------------- The latest development version can be installed directly from GitHub: .. code-block:: bash # Mac OS X via Homebrew $ brew install httpie --HEAD # Universal $ pip install --upgrade https://github.com/jkbrzt/httpie/archive/master.tar.gz Python version -------------- Although Python 2.6 and 2.7 are supported as well, it is recommended to install HTTPie against the latest Python 3.x whenever possible. That will ensure that some of the newer HTTP features, such as `SNI (Server Name Indication)`_, work out of the box. Python 3 is the default for Homebrew installations starting with version 0.9.4. To see which version HTTPie uses, run ``http --debug``. Usage ===== Hello World: .. code-block:: bash $ http httpie.org Synopsis: .. code-block:: bash $ http [flags] [METHOD] URL [ITEM [ITEM]] See also ``http --help``. Examples -------- Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data: .. code-block:: bash $ http PUT example.org X-API-Token:123 name=John Submitting `forms`_: .. code-block:: bash $ http -f POST example.org hello=World See the request that is being sent using one of the `output options`_: .. code-block:: bash $ http -v example.org Use `Github API`_ to post a comment on an `issue `_ with `authentication`_: .. code-block:: bash $ http -a USERNAME POST https://api.github.com/repos/jkbrzt/httpie/issues/83/comments body='HTTPie is awesome! :heart:' Upload a file using `redirected input`_: .. code-block:: bash $ http example.org < file.json Download a file and save it via `redirected output`_: .. code-block:: bash $ http example.org/file > file Download a file ``wget`` style: .. code-block:: bash $ http --download example.org/file Use named `sessions`_ to make certain aspects or the communication persistent between requests to the same host: .. code-block:: bash $ http --session=logged-in -a username:password httpbin.org/get API-Key:123 $ http --session=logged-in httpbin.org/headers Set a custom ``Host`` header to work around missing DNS records: .. code-block:: bash $ http localhost:8000 Host:example.com .. HTTP method =========== The name of the HTTP method comes right before the URL argument: .. code-block:: bash $ http DELETE example.org/todos/7 Which looks similar to the actual ``Request-Line`` that is sent: .. code-block:: http DELETE /todos/7 HTTP/1.1 When the ``METHOD`` argument is omitted from the command, HTTPie defaults to either ``GET`` (with no request data) or ``POST`` (with request data). Request URL =========== The only information HTTPie needs to perform a request is a URL. The default scheme is, somewhat unsurprisingly, ``http://``, and can be omitted from the argument – ``http example.org`` works just fine. Querystring parameters ---------------------- If you find yourself manually constructing URLs with on the terminal, you may appreciate the ``param==value`` syntax for appending URL parameters. With that, you don't have to worry about escaping the ``&`` separators for your shell. Also, special characters in parameter values, will also automatically escaped (HTTPie otherwise expects the URL to be already escaped). To search for ``HTTPie logo`` on Google Images you could use this command: .. code-block:: bash $ http www.google.com search=='HTTPie logo' tbm==isch .. code-block:: http GET /?search=HTTPie+logo&tbm=isch HTTP/1.1 URL shortcuts for ``localhost`` ------------------------------- Additionally, curl-like shorthand for localhost is supported. This means that, for example ``:3000`` would expand to ``http://localhost:3000`` If the port is omitted, then port 80 is assumed. .. code-block:: bash $ http :/foo .. code-block:: http GET /foo HTTP/1.1 Host: localhost .. code-block:: bash $ http :3000/bar .. code-block:: http GET /bar HTTP/1.1 Host: localhost:3000 .. code-block:: bash $ http : .. code-block:: http GET / HTTP/1.1 Host: localhost Custom default scheme --------------------- You can use the ``--default-scheme `` option to create shortcuts for other protocols than HTTP: .. code-block:: bash $ alias https='http --default-scheme=https' Request items ============= There are a few different *request item* types that provide a convenient mechanism for specifying HTTP headers, simple JSON and form data, files, and URL parameters. They are key/value pairs specified after the URL. All have in common that they become part of the actual request that is sent and that their type is distinguished only by the separator used: ``:``, ``=``, ``:=``, ``==``, ``@``, ``=@``, and ``:=@``. The ones with an ``@`` expect a file path as value. +-----------------------+-----------------------------------------------------+ | Item Type | Description | +=======================+=====================================================+ | HTTP Headers | Arbitrary HTTP header, e.g. ``X-API-Token:123``. | | ``Name:Value`` | | +-----------------------+-----------------------------------------------------+ | URL parameters | Appends the given name/value pair as a query | | ``name==value`` | string parameter to the URL. | | | The ``==`` separator is used. | +-----------------------+-----------------------------------------------------+ | Data Fields | Request data fields to be serialized as a JSON | | ``field=value``, | object (default), or to be form-encoded | | ``field=@file.txt`` | (``--form, -f``). | +-----------------------+-----------------------------------------------------+ | Raw JSON fields | Useful when sending JSON and one or | | ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, | | ``field:=@file.json`` | nested ``Object``, or an ``Array``, e.g., | | | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` | | | (note the quotes). | +-----------------------+-----------------------------------------------------+ | Form File Fields | Only available with ``--form, -f``. | | ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. | | | The presence of a file field results | | | in a ``multipart/form-data`` request. | +-----------------------+-----------------------------------------------------+ Note that data fields aren't the only way to specify request data: `Redirected input`_ is a mechanism for passing arbitrary data request request. Escaping rules -------------- You can use ``\`` to escape characters that shouldn't be used as separators (or parts thereof). For instance, ``foo\==bar`` will become a data key/value pair (``foo=`` and ``bar``) instead of a URL parameter. Often it is necessary to quote the values, e.g. ``foo='bar baz'``. If any of the field names or headers starts with a minus (e.g., ``-fieldname``), you need to place all such items after the special token ``--`` to prevent confusion with ``--arguments``: .. code-block:: bash $ http httpbin.org/post -- -name-starting-with-dash=foo -Unusual-Header:bar .. code-block:: http POST /post HTTP/1.1 -Unusual-Header: bar Content-Type: application/json { "-name-starting-with-dash": "value" } JSON ==== JSON is the *lingua franca* of modern web services and it is also the **implicit content type** HTTPie by default uses. Simple example: .. code-block:: bash $ http PUT example.org name=John email=john@example.org .. code-block:: http PUT / HTTP/1.1 Accept: application/json, */* Accept-Encoding: gzip, deflate Content-Type: application/json Host: example.org { "name": "John", "email": "john@example.org" } Default behaviour ----------------- If your command includes some data `request items`_, they are serialized as a JSON object by default. HTTPie also automatically sets the following headers, both of which can be overwritten: ================ ======================================= ``Content-Type`` ``application/json`` ``Accept`` ``application/json, */*`` ================ ======================================= Explicit JSON ------------- You can use ``--json, -j`` to explicitly set ``Accept`` to ``application/json`` regardless of whether you are sending data (it's a shortcut for setting the header via the usual header notation: ``http url Accept:'application/json, */*'``). Additionally, HTTPie will try to detect JSON responses even when the ``Content-Type`` is incorrectly ``text/plain`` or unknown. Non-string JSON fields ---------------------- Non-string fields use the ``:=`` separator, which allows you to embed raw JSON into the resulting object. Text and raw JSON files can also be embedded into fields using ``=@`` and ``:=@``: .. code-block:: bash $ http PUT api.example.com/person/1 \ name=John \ age:=29 married:=false hobbies:='["http", "pies"]' \ # Raw JSON description=@about-john.txt \ # Embed text file bookmarks:=@bookmarks.json # Embed JSON file .. code-block:: http PUT /person/1 HTTP/1.1 Accept: application/json, */* Content-Type: application/json Host: api.example.com { "age": 29, "hobbies": [ "http", "pies" ], "description": "John is a nice guy who likes pies.", "married": false, "name": "John", "bookmarks": { "HTTPie": "http://httpie.org", } } Please note that with this syntax the command gets unwieldy when sending complex data. In that case it's always better to use `redirected input`_: .. code-block:: bash $ http POST api.example.com/person/1 < person.json Forms ===== Submitting forms is very similar to sending `JSON`_ requests. Often the only difference is in adding the ``--form, -f`` option, which ensures that data fields are serialized as, and ``Content-Type`` is set to, ``application/x-www-form-urlencoded; charset=utf-8``. It is possible to make form data the implicit content type instead of JSON via the `config`_ file. Regular forms ------------- .. code-block:: bash $ http --form POST api.example.org/person/1 name='John Smith' .. code-block:: http POST /person/1 HTTP/1.1 Content-Type: application/x-www-form-urlencoded; charset=utf-8 name=John+Smith File upload forms ----------------- If one or more file fields is present, the serialization and content type is ``multipart/form-data``: .. code-block:: bash $ http -f POST example.com/jobs name='John Smith' cv@~/Documents/cv.pdf The request above is the same as if the following HTML form were submitted: .. code-block:: html
Note that ``@`` is used to simulate a file upload form field, whereas ``=@`` just embeds the file content as a regular text field value. HTTP headers ============ To set custom headers you can use the ``Header:Value`` notation: .. code-block:: bash $ http example.org User-Agent:Bacon/1.0 'Cookie:valued-visitor=yes;foo=bar' \ X-Foo:Bar Referer:http://httpie.org/ .. code-block:: http GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Cookie: valued-visitor=yes;foo=bar Host: example.org Referer: http://httpie.org/ User-Agent: Bacon/1.0 X-Foo: Bar Default request headers ----------------------- There are a couple of default headers that HTTPie sets: .. code-block:: http GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate User-Agent: HTTPie/ Host: Any of those—except for ``Host``—can be overwritten and some of them unset. Empty headers and header un-setting ----------------------------------- To unset a previously specified header (such a one of the default headers), use ``Header:``: .. code-block:: bash $ http httpbin.org/headers Accept: User-Agent: To send a header with an empty value, use ``Header;``: .. code-block:: bash $ http httpbin.org/headers 'Header;' Authentication ============== The currently supported authentication schemes are Basic and Digest (see `auth plugins`_ for more). There are two flags that control authentication: =================== ====================================================== ``--auth, -a`` Pass a ``username:password`` pair as the argument. Or, if you only specify a username (``-a username``), you'll be prompted for the password before the request is sent. To send an empty password, pass ``username:``. The ``username:password@hostname`` URL syntax is supported as well (but credentials passed via ``-a`` have higher priority). ``--auth-type, -A`` Specify the auth mechanism. Possible values are ``basic`` and ``digest``. The default value is ``basic`` so it can often be omitted. =================== ====================================================== Basic auth ---------- .. code-block:: bash $ http -a username:password example.org Digest auth ----------- .. code-block:: bash $ http -A digest -a username:password example.org Password prompt --------------- .. code-block:: bash $ http -a username example.org ``.netrc`` ---------- Authorization information from your ``~/.netrc`` file is honored as well: .. code-block:: bash $ cat ~/.netrc machine httpbin.org login httpie password test $ http httpbin.org/basic-auth/httpie/test HTTP/1.1 200 OK [...] Auth plugins ------------ Additional authentication mechanism can be installed as plugins. They can be found on the `Python Package Index `_. Here's a few picks: * `httpie-api-auth `_: ApiAuth * `httpie-aws-auth `_: ApiAuth * `httpie-edgegrid `_: EdgeGrid * `httpie-hmac-auth `_: HMAC * `httpie-jwt-auth `_: JWTAuth (JSON Web Tokens) * `httpie-negotiate `_: SPNEGO (GSS Negotiate) * `httpie-ntlm `_: NTLM (NT LAN Manager) * `httpie-oauth `_: OAuth * `requests-hawk `_: Hawk HTTP redirects ============== By default, HTTP redirects are not followed and only the first response is shown: .. code-block:: bash $ http httpbin.org/redirect/3 Follow ``Location`` ------------------- To instruct HTTPie to follow the ``Location`` header of ``30x`` responses and show the final response instead, use the ``--follow, -F`` option: .. code-block:: bash $ http --follow httpbin.org/redirect/3 Showing intermediary redirect responses --------------------------------------- If you additionally wish to see the intermediary requests/responses, then use the ``--all`` option as well: .. code-block:: bash $ http --follow --all httpbin.org/redirect/3 Limiting maximum redirects followed ----------------------------------- To change the default limit of maximum ``30`` redirects, use the ``--max-redirects=`` option: .. code-block:: bash $ http --follow --all --max-redirects=5 httpbin.org/redirect/3 Proxies ======= You can specify proxies to be used through the ``--proxy`` argument for each protocol (which is included in the value in case of redirects across protocols): .. code-block:: bash $ http --proxy=http:http://10.10.1.10:3128 --proxy=https:https://10.10.1.10:1080 example.org With Basic authentication: .. code-block:: bash $ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org Environment variables --------------------- You can also configure proxies by environment variables ``HTTP_PROXY`` and ``HTTPS_PROXY``, and the underlying Requests library will pick them up as well. If you want to disable proxies configured through the environment variables for certain hosts, you can specify them in ``NO_PROXY``. In your ``~/.bash_profile``: .. code-block:: bash export HTTP_PROXY=http://10.10.1.10:3128 export HTTPS_PROXY=https://10.10.1.10:1080 export NO_PROXY=localhost,example.com SOCKS ----- To enable SOCKS proxy support please install ``requests[socks]`` using ``pip``: .. code-block:: bash $ pip install -U requests[socks] Usage is the same as for other types of `proxies`_: .. code-block:: bash $ http --proxy=http:socks5://user:pass@host:port --proxy=https:socks5://user:pass@host:port example.org HTTPS ===== Server SSL certificate verification ----------------------------------- To skip the host's SSL certificate verification, you can pass ``--verify=no`` (default is ``yes``): .. code-block:: bash $ http --verify=no https://example.org Custom CA bundle ---------------- You can also use ``--verify=`` to set a custom CA bundle path: .. code-block:: bash $ http --verify=/ssl/custom_ca_bundle https://example.org Client side SSL certificate --------------------------- To use a client side certificate for the SSL communication, you can pass the path of the cert file with ``--cert``: .. code-block:: bash $ http --cert=client.pem https://example.org If the private key is not contained in the cert file you may pass the path of the key file with ``--cert-key``: .. code-block:: bash $ http --cert=client.crt --cert-key=client.key https://example.org SSL version ----------- Use the ``--ssl=`` to specify the desired protocol version to use. This will default to SSL v2.3 which will negotiate the highest protocol that both the server and your installation of OpenSSL support. The available protocols are ``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ``tls1.2``. (The actually available set of protocols may vary depending on your OpenSSL installation.) .. code-block:: bash # Specify the vulnerable SSL v3 protocol to talk to an outdated server: $ http --ssl=ssl3 https://vulnerable.example.org SNI (Server Name Indication) ---------------------------- If you use HTTPie with `Python version`_ lower than 2.7.9 (can be verified with ``http --debug``) and need to talk to servers that use SNI (Server Name Indication) you need to install some additional dependencies: .. code-block:: bash $ pip install --upgrade requests[security] You can use the following command to test SNI support: .. code-block:: bash $ http https://sni.velox.ch Output options ============== By default, HTTPie only outputs the final response and the whole response message is printed (headers as well as the body). You can control what should be printed via several options: ================= ===================================================== ``--headers, -h`` Only the response headers are printed. ``--body, -b`` Only the response body is printed. ``--verbose, -v`` Print the whole HTTP exchange (request and response). This option also enables ``--all`` (see bellow). ``--print, -p`` Selects parts of the HTTP exchange. ================= ===================================================== ``--verbose`` can often be useful for debugging the request and generating documentation examples: .. code-block:: bash $ http --verbose PUT httpbin.org/put hello=world PUT /put HTTP/1.1 Accept: application/json, */* Accept-Encoding: gzip, deflate Content-Type: application/json Host: httpbin.org User-Agent: HTTPie/0.2.7dev { "hello": "world" } HTTP/1.1 200 OK Connection: keep-alive Content-Length: 477 Content-Type: application/json Date: Sun, 05 Aug 2012 00:25:23 GMT Server: gunicorn/0.13.4 { […] } What parts of the HTTP exchange should be printed ------------------------------------------------- All the other `output options`_ are under the hood just shortcuts for the more powerful ``--print, -p``. It accepts a string of characters each of which represents a specific part of the HTTP exchange: ========== ================== Character Stands for ========== ================== ``H`` request headers ``B`` request body ``h`` response headers ``b`` response body ========== ================== Print request and response headers: .. code-block:: bash $ http --print=Hh PUT httpbin.org/put hello=world Viewing intermediary requests/responses --------------------------------------- To see all the HTTP communication, i.e. the final request/response as well as any possible intermediary requests/responses, use the ``--all`` option. The intermediary HTTP communication include followed redirects (with ``--follow``), the first unauthorized request when HTTP digest authentication is used (``--auth=digest``), etc. .. code-block:: bash # Include all responses that lead to the final one: $ http --all --follow httpbin.org/redirect/3 The intermediary requests/response are by default formatted according to ``--print, -p`` (and its shortcuts described above). If you'd like to change that, use the ``--history-print, -P`` option. It takes the same arguments as ``--print, -p`` but applies to the intermediary requests only. .. code-block:: bash # Print the intermediary requests/responses differently than the final one: $ http -A digest -a foo:bar --all -p Hh -P H httpbin.org/digest-auth/auth/foo/bar Conditional body download ------------------------- As an optimization, the response body is downloaded from the server only if it's part of the output. This is similar to performing a ``HEAD`` request, except that it applies to any HTTP method you use. Let's say that there is an API that returns the whole resource when it is updated, but you are only interested in the response headers to see the status code after an update: .. code-block:: bash $ http --headers PATCH example.org/Really-Huge-Resource name='New Name' Since we are only printing the HTTP headers here, the connection to the server is closed as soon as all the response headers have been received. Therefore, bandwidth and time isn't wasted downloading the body which you don't care about. The response headers are downloaded always, even if they are not part of the output Redirected Input ================ The universal method for passing request data is through redirected ``stdin`` (standard input)—piping. Such data is buffered and then with no further processing used as the request body. There are multiple useful ways to use piping: Redirect from a file: .. code-block:: bash $ http PUT example.com/person/1 X-API-Token:123 < person.json Or the output of another program: .. code-block:: bash $ grep '401 Unauthorized' /var/log/httpd/error_log | http POST example.org/intruders You can use ``echo`` for simple data: .. code-block:: bash $ echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123 You can even pipe web services together using HTTPie: .. code-block:: bash $ http GET https://api.github.com/repos/jkbrzt/httpie | http POST httpbin.org/post You can use ``cat`` to enter multiline data on the terminal: .. code-block:: bash $ cat | http POST example.com ^D .. code-block:: bash $ cat | http POST example.com/todos Content-Type:text/plain - buy milk - call parents ^D On OS X, you can send the contents of the clipboard with ``pbpaste``: .. code-block:: bash $ pbpaste | http PUT example.com Passing data through ``stdin`` cannot be combined with data fields specified on the command line: .. code-block:: bash $ echo 'data' | http POST example.org more=data # This is invalid To prevent HTTPie from reading ``stdin`` data you can use the ``--ignore-stdin`` option. Request data from a filename ---------------------------- An alternative to redirected ``stdin`` is specifying a filename (as ``@/path/to/file``) whose content is used as if it came from ``stdin``. It has the advantage that the ``Content-Type`` header is automatically set to the appropriate value based on the filename extension. For example, the following request sends the verbatim contents of that XML file with ``Content-Type: application/xml``: .. code-block:: bash $ http PUT httpbin.org/put @/data/file.xml Terminal output =============== HTTPie does several things by default in order to make its terminal output easy to read. Colors and formatting --------------------- Syntax highlighting is applied to HTTP headers and bodies (where it makes sense). You can choose your preferred color scheme via the ``--style`` option if you don't like the default one (see ``$ http --help`` for the possible values). Also, the following formatting is applied: * HTTP headers are sorted by name. * JSON data is indented, sorted by keys, and unicode escapes are converted to the characters they represent. One of these options can be used to control output processing: ==================== ======================================================== ``--pretty=all`` Apply both colors and formatting. Default for terminal output. ``--pretty=colors`` Apply colors. ``--pretty=format`` Apply formatting. ``--pretty=none`` Disables output processing. Default for redirected output. ==================== ======================================================== Binary data ----------- Binary data is suppressed for terminal output, which makes it safe to perform requests to URLs that send back binary data. Binary data is suppressed also in redirected, but prettified output. The connection is closed as soon as we know that the response body is binary, .. code-block:: bash $ http example.org/Movie.mov You will nearly instantly see something like this: .. code-block:: http HTTP/1.1 200 OK Accept-Ranges: bytes Content-Encoding: gzip Content-Type: video/quicktime Transfer-Encoding: chunked +-----------------------------------------+ | NOTE: binary data not shown in terminal | +-----------------------------------------+ Redirected output ================= HTTPie uses a different set of defaults for redirected output than for `terminal output`_. The differences being: * Formatting and colors aren't applied (unless ``--pretty`` is specified). * Only the response body is printed (unless one of the `output options`_ is set). * Also, binary data isn't suppressed. The reason is to make piping HTTPie's output to another programs and downloading files work with no extra flags. Most of the time, only the raw response body is of an interest when the output is redirected. Download a file: .. code-block:: bash $ http example.org/Movie.mov > Movie.mov Download an image of Octocat, resize it using ImageMagick, upload it elsewhere: .. code-block:: bash $ http octodex.github.com/images/original.jpg | convert - -resize 25% - | http example.org/Octocats Force colorizing and formatting, and show both the request and the response in ``less`` pager: .. code-block:: bash $ http --pretty=all --verbose example.org | less -R The ``-R`` flag tells ``less`` to interpret color escape sequences included HTTPie`s output. You can create a shortcut for invoking HTTPie with colorized and paged output by adding the following to your ``~/.bash_profile``: .. code-block:: bash function httpless { # `httpless example.org' http --pretty=all --print=hb "$@" | less -R; } Download mode ============= HTTPie features a download mode in which it acts similarly to ``wget``. When enabled using the ``--download, -d`` flag, response headers are printed to the terminal (``stderr``), and a progress bar is shown while the response body is being saved to a file. .. code-block:: bash $ http --download https://github.com/jkbrzt/httpie/archive/master.tar.gz .. code-block:: http HTTP/1.1 200 OK Content-Disposition: attachment; filename=httpie-master.tar.gz Content-Length: 257336 Content-Type: application/x-gzip Downloading 251.30 kB to "httpie-master.tar.gz" Done. 251.30 kB in 2.73862s (91.76 kB/s) Downloaded file name -------------------- If not provided via ``--output, -o``, the output filename will be determined from ``Content-Disposition`` (if available), or from the URL and ``Content-Type``. If the guessed filename already exists, HTTPie adds a unique suffix to it. Piping while downloading ------------------------ You can also redirect the response body to another program while the response headers and progress are still shown in the terminal: .. code-block:: bash $ http -d https://github.com/jkbrzt/httpie/archive/master.tar.gz | tar zxf - Resuming downloads ------------------ If ``--output, -o`` is specified, you can resume a partial download using the ``--continue, -c`` option. This only works with servers that support ``Range`` requests and ``206 Partial Content`` responses. If the server doesn't support that, the whole file will simply be downloaded: .. code-block:: bash $ http -dco file.zip example.org/file Other notes ----------- * The ``--download`` option only changes how the response body is treated. * You can still set custom headers, use sessions, ``--verbose, -v``, etc. * ``--download`` always implies ``--follow`` (redirects are followed). * HTTPie exits with status code ``1`` (error) if the body hasn't been fully downloaded. * ``Accept-Encoding`` cannot be set with ``--download``. Streamed responses ================== Responses are downloaded and printed in chunks which allows for streaming and large file downloads without using too much memory. However, when `colors and formatting`_ is applied, the whole response is buffered and only then processed at once. Disabling buffering ------------------- You can use the ``--stream, -S`` flag to make two things happen: 1. The output is flushed in much smaller chunks without any buffering, which makes HTTPie behave kind of like ``tail -f`` for URLs. 2. Streaming becomes enabled even when the output is prettified: It will be applied to each line of the response and flushed immediately. This makes it possible to have a nice output for long-lived requests, such as one to the Twitter streaming API. Examples use cases ------------------ Prettified streamed response: .. code-block:: bash $ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track='Justin Bieber' Streamed output by small chunks alá ``tail -f``: .. code-block:: bash # Send each new tweet (JSON object) mentioning "Apple" to another # server as soon as it arrives from the Twitter streaming API: $ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track=Apple \ | while read tweet; do echo "$tweet" | http POST example.org/tweets ; done Sessions ======== By default, every request HTTPie makes is completely independent of any previous ones to the same host. However, HTTPie also supports persistent sessions via the ``--session=SESSION_NAME_OR_PATH`` option. In a session, custom headers—except for the ones starting with ``Content-`` or ``If-``—, authorization, and cookies (manually specified or sent by the server) persist between requests to the same host. .. code-block:: bash # Create a new session $ http --session=/tmp/session.json example.org API-Token:123 # Re-use an existing session — API-Token will be set: $ http --session=/tmp/session.json example.org All session data, including credentials, cookie data, and custom headers are stored in plain text. That means session files can also be created and edited manually in a text editor—they are regular JSON. Named sessions -------------- You can create one or more named session per host. For example, this is how you can create a new session named ``user1`` for ``example.org``: .. code-block:: bash $ http --session=user1 -a user1:password example.org X-Foo:Bar From now onw, you can refer to the session by its name. When you choose to use the session again, any the previously used authorization and HTTP headers will automatically be set: .. code-block:: bash $ http --session=user1 example.org To create or reuse a different session, simple specify a different name: .. code-block:: bash $ http --session=user2 -a user2:password example.org X-Bar:Foo Named sessions' data is stored in JSON files in the directory ``~/.httpie/sessions//.json`` (``%APPDATA%\httpie\sessions\\.json`` on Windows). Anonymous sessions ------------------ Instead of a name, you can also directly specify a path to a session file. This allows for sessions to be re-used across multiple hosts: .. code-block:: bash $ http --session=/tmp/session.json example.org $ http --session=/tmp/session.json admin.example.org $ http --session=~/.httpie/sessions/another.example.org/test.json example.org $ http --session-read-only=/tmp/session.json example.org Readonly session ---------------- To use an existing session file without updating it from the request/response exchange once it is created, specify the session name via ``--session-read-only=SESSION_NAME_OR_PATH`` instead. Config ====== HTTPie uses a simple JSON config file. Config file location -------------------- The default location of the configuration file is ``~/.httpie/config.json`` (or ``%APPDATA%\httpie\config.json`` on Windows). The config directory location can be changed by setting the ``HTTPIE_CONFIG_DIR`` environment variable. To view the exact location run ``http --debug``. Configurable options -------------------- The JSON file contains an object with the following keys: ``default_options`` ~~~~~~~~~~~~~~~~~~~ An ``Array`` (by default empty) of default options that should be applied to every invocation of HTTPie. For instance, you can use this option to change the default style and output options: ``"default_options": ["--style=fruity", "--body"]`` Another useful default option could be ``"--session=default"`` to make HTTPie always use `sessions`_ (one named ``default`` will automatically be used). Or you could change the implicit request content type from JSON to form by adding ``--form`` to the list. ``__meta__`` ~~~~~~~~~~~~ HTTPie automatically stores some of its metadata here. Please do not change. Un-setting previously specified options --------------------------------------- Default options from the config file, or specified any other way, can be unset for a particular invocation via ``--no-OPTION`` arguments passed on the command line (e.g., ``--no-style`` or ``--no-session``). Scripting ========= When using HTTPie from shell scripts, it can be handy to set the ``--check-status`` flag. It instructs HTTPie to exit with an error if the HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will be ``3`` (unless ``--follow`` is set), ``4``, or ``5``, respectively. .. code-block:: bash #!/bin/bash if http --check-status --ignore-stdin --timeout=2.5 HEAD example.org/health &> /dev/null; then echo 'OK!' else case $? in 2) echo 'Request timed out!' ;; 3) echo 'Unexpected HTTP 3xx Redirection!' ;; 4) echo 'HTTP 4xx Client Error!' ;; 5) echo 'HTTP 5xx Server Error!' ;; 6) echo 'Exceeded --max-redirects= redirects!' ;; *) echo 'Other Error!' ;; esac fi Best practices -------------- The default behaviour of automatically reading ``stdin`` is typically not desirable during non-interactive invocations. You most likely want use the ``--ignore-stdin`` option to disable it. It is a common gotcha that without this option HTTPie seemingly hangs. What happens is that when HTTPie is invoked for example from a cron job, ``stdin`` is not connected to a terminal. Therefore, rules for `redirected input`_ apply, i.e., HTTPie starts to read it expecting that the request body will be passed through. And since there's no data nor ``EOF``, it will be stuck. So unless you're piping some data to HTTPie, this flag should be used in scripts. Also, it's might be good to override the default ``30`` second ``--timeout`` to something that suits you. Meta ==== Interface design ---------------- The syntax of the command arguments closely corresponds to the actual HTTP requests sent over the wire. It has the advantage that it's easy to remember and read. It is often possible to translate an HTTP request to an HTTPie argument list just by inlining the request elements. For example, compare this HTTP request: .. code-block:: http POST /collection HTTP/1.1 X-API-Key: 123 User-Agent: Bacon/1.0 Content-Type: application/x-www-form-urlencoded name=value&name2=value2 with the HTTPie command that sends it: .. code-block:: bash $ http -f POST example.org/collection \ X-API-Key:123 \ User-Agent:Bacon/1.0 \ name=value \ name2=value2 Notice that both the order of elements and the syntax is very similar, and that only a small portion of the command is used to control HTTPie and doesn't directly correspond to any part of the request (here it's only ``-f`` asking HTTPie to send a form request). The two modes, ``--pretty=all`` (default for terminal) and ``--pretty=none`` (default for redirected output), allow for both user-friendly interactive use and usage from scripts, where HTTPie serves as a generic HTTP client. As HTTPie is still under heavy development, the existing command line syntax and some of the ``--OPTIONS`` may change slightly before HTTPie reaches its final version ``1.0``. All changes are recorded in the `change log`_. User support ------------ Please use the following support channels: * `GitHub issues `_ for bug reports and feature requests. * `Our Gitter chat room `_ to ask questions, discuss features, and for general discussion. * `StackOverflow `_ to ask questions (please make sure to use the `httpie `_ tag). * Tweet directly to `@clihttp `_. * You can also tweet directly to `@jkbrzt`_. Related projects ---------------- Dependencies ~~~~~~~~~~~~ Under the hood, HTTPie uses these two amazing libraries: * `Requests `_ — Python HTTP library for humans * `Pygments `_ — Python syntax highlighter HTTPie friends ~~~~~~~~~~~~~~ HTTPie plays exceptionally well with the following tools: * `jq `_ — CLI JSON processor that works great in conjunction with HTTPie * `http-prompt `_ — interactive shell for HTTPie featuring autocomplete and command syntax highlighting Contributing ------------ See `CONTRIBUTING.rst `_. Change log ---------- See `CHANGELOG `_. Artwork ------- See `claudiatd/httpie-artwork`_ Licence ------- BSD-3-Clause: `LICENSE `_. Authors ------- `Jakub Roztocil`_ (`@jkbrzt`_) created HTTPie and `these fine people`_ have contributed. .. _pip: http://www.pip-installer.org/en/latest/index.html .. _Github API: http://developer.github.com/v3/issues/comments/#create-a-comment .. _these fine people: https://github.com/jkbrzt/httpie/contributors .. _Jakub Roztocil: http://roztocil.co .. _@jkbrzt: https://twitter.com/jkbrzt .. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork .. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version :target: https://pypi.python.org/pypi/httpie :alt: Latest version released on PyPi .. |coverage| image:: https://img.shields.io/coveralls/jkbrzt/httpie/master.svg?style=flat-square&label=coverage :target: https://coveralls.io/r/jkbrzt/httpie?branch=master :alt: Test coverage .. |unix_build| image:: https://img.shields.io/travis/jkbrzt/httpie/master.svg?style=flat-square&label=unix%20build :target: http://travis-ci.org/jkbrzt/httpie :alt: Build status of the master branch on Mac/Linux .. |windows_build| image:: https://img.shields.io/appveyor/ci/jkbrzt/httpie.svg?style=flat-square&label=windows%20build :target: https://ci.appveyor.com/project/jkbrzt/httpie :alt: Build status of the master branch on Windows .. |gitter| image:: https://badges.gitter.im/jkbrzt/httpie.svg :target: https://gitter.im/jkbrzt/httpie :alt: Chat on Gitter httpie-0.9.8/setup.cfg0000644000000000000000000000002613022157774013352 0ustar rootroot[wheel] universal = 1 httpie-0.9.8/httpie/0000755000000000000000000000000013022157774013030 5ustar rootroothttpie-0.9.8/httpie/downloads.py0000644000000000000000000003415713022157774015406 0ustar rootroot# coding=utf-8 """ Download mode implementation. """ from __future__ import division import os import re import sys import errno import mimetypes import threading from time import sleep, time from mailbox import Message from httpie.output.streams import RawStream from httpie.models import HTTPResponse from httpie.utils import humanize_bytes from httpie.compat import urlsplit PARTIAL_CONTENT = 206 CLEAR_LINE = '\r\033[K' PROGRESS = ( '{percentage: 6.2f} %' ' {downloaded: >10}' ' {speed: >10}/s' ' {eta: >8} ETA' ) PROGRESS_NO_CONTENT_LENGTH = '{downloaded: >10} {speed: >10}/s' SUMMARY = 'Done. {downloaded} in {time:0.5f}s ({speed}/s)\n' SPINNER = '|/-\\' class ContentRangeError(ValueError): pass def parse_content_range(content_range, resumed_from): """ Parse and validate Content-Range header. :param content_range: the value of a Content-Range response header eg. "bytes 21010-47021/47022" :param resumed_from: first byte pos. from the Range request header :return: total size of the response body when fully downloaded. """ if content_range is None: raise ContentRangeError('Missing Content-Range') pattern = ( '^bytes (?P\d+)-(?P\d+)' '/(\*|(?P\d+))$' ) match = re.match(pattern, content_range) if not match: raise ContentRangeError( 'Invalid Content-Range format %r' % content_range) content_range_dict = match.groupdict() first_byte_pos = int(content_range_dict['first_byte_pos']) last_byte_pos = int(content_range_dict['last_byte_pos']) instance_length = ( int(content_range_dict['instance_length']) if content_range_dict['instance_length'] else None ) # "A byte-content-range-spec with a byte-range-resp-spec whose # last- byte-pos value is less than its first-byte-pos value, # or whose instance-length value is less than or equal to its # last-byte-pos value, is invalid. The recipient of an invalid # byte-content-range- spec MUST ignore it and any content # transferred along with it." if (first_byte_pos >= last_byte_pos or (instance_length is not None and instance_length <= last_byte_pos)): raise ContentRangeError( 'Invalid Content-Range returned: %r' % content_range) if (first_byte_pos != resumed_from or (instance_length is not None and last_byte_pos + 1 != instance_length)): # Not what we asked for. raise ContentRangeError( 'Unexpected Content-Range returned (%r)' ' for the requested Range ("bytes=%d-")' % (content_range, resumed_from) ) return last_byte_pos + 1 def filename_from_content_disposition(content_disposition): """ Extract and validate filename from a Content-Disposition header. :param content_disposition: Content-Disposition value :return: the filename if present and valid, otherwise `None` """ # attachment; filename=jkbrzt-httpie-0.4.1-20-g40bd8f6.tar.gz msg = Message('Content-Disposition: %s' % content_disposition) filename = msg.get_filename() if filename: # Basic sanitation. filename = os.path.basename(filename).lstrip('.').strip() if filename: return filename def filename_from_url(url, content_type): fn = urlsplit(url).path.rstrip('/') fn = os.path.basename(fn) if fn else 'index' if '.' not in fn and content_type: content_type = content_type.split(';')[0] if content_type == 'text/plain': # mimetypes returns '.ksh' ext = '.txt' else: ext = mimetypes.guess_extension(content_type) if ext == '.htm': # Python 3 ext = '.html' if ext: fn += ext return fn def trim_filename(filename, max_len): if len(filename) > max_len: trim_by = len(filename) - max_len name, ext = os.path.splitext(filename) if trim_by >= len(name): filename = filename[:-trim_by] else: filename = name[:-trim_by] + ext return filename def get_filename_max_length(directory): max_len = 255 try: pathconf = os.pathconf except AttributeError: pass # non-posix else: try: max_len = pathconf(directory, 'PC_NAME_MAX') except OSError as e: if e.errno != errno.EINVAL: raise return max_len def trim_filename_if_needed(filename, directory='.', extra=0): max_len = get_filename_max_length(directory) - extra if len(filename) > max_len: filename = trim_filename(filename, max_len) return filename def get_unique_filename(filename, exists=os.path.exists): attempt = 0 while True: suffix = '-' + str(attempt) if attempt > 0 else '' try_filename = trim_filename_if_needed(filename, extra=len(suffix)) try_filename += suffix if not exists(try_filename): return try_filename attempt += 1 class Downloader(object): def __init__(self, output_file=None, resume=False, progress_file=sys.stderr): """ :param resume: Should the download resume if partial download already exists. :type resume: bool :param output_file: The file to store response body in. If not provided, it will be guessed from the response. :param progress_file: Where to report download progress. """ self._output_file = output_file self._resume = resume self._resumed_from = 0 self.finished = False self.status = Status() self._progress_reporter = ProgressReporterThread( status=self.status, output=progress_file ) def pre_request(self, request_headers): """Called just before the HTTP request is sent. Might alter `request_headers`. :type request_headers: dict """ # Ask the server not to encode the content so that we can resume, etc. request_headers['Accept-Encoding'] = 'identity' if self._resume: bytes_have = os.path.getsize(self._output_file.name) if bytes_have: # Set ``Range`` header to resume the download # TODO: Use "If-Range: mtime" to make sure it's fresh? request_headers['Range'] = 'bytes=%d-' % bytes_have self._resumed_from = bytes_have def start(self, response): """ Initiate and return a stream for `response` body with progress callback attached. Can be called only once. :param response: Initiated response object with headers already fetched :type response: requests.models.Response :return: RawStream, output_file """ assert not self.status.time_started # FIXME: some servers still might sent Content-Encoding: gzip # try: total_size = int(response.headers['Content-Length']) except (KeyError, ValueError, TypeError): total_size = None if self._output_file: if self._resume and response.status_code == PARTIAL_CONTENT: total_size = parse_content_range( response.headers.get('Content-Range'), self._resumed_from ) else: self._resumed_from = 0 try: self._output_file.seek(0) self._output_file.truncate() except IOError: pass # stdout else: # TODO: Should the filename be taken from response.history[0].url? # Output file not specified. Pick a name that doesn't exist yet. filename = None if 'Content-Disposition' in response.headers: filename = filename_from_content_disposition( response.headers['Content-Disposition']) if not filename: filename = filename_from_url( url=response.url, content_type=response.headers.get('Content-Type'), ) self._output_file = open(get_unique_filename(filename), mode='a+b') self.status.started( resumed_from=self._resumed_from, total_size=total_size ) stream = RawStream( msg=HTTPResponse(response), with_headers=False, with_body=True, on_body_chunk_downloaded=self.chunk_downloaded, chunk_size=1024 * 8 ) self._progress_reporter.output.write( 'Downloading %sto "%s"\n' % ( (humanize_bytes(total_size) + ' ' if total_size is not None else ''), self._output_file.name ) ) self._progress_reporter.start() return stream, self._output_file def finish(self): assert not self.finished self.finished = True self.status.finished() def failed(self): self._progress_reporter.stop() @property def interrupted(self): return ( self.finished and self.status.total_size and self.status.total_size != self.status.downloaded ) def chunk_downloaded(self, chunk): """ A download progress callback. :param chunk: A chunk of response body data that has just been downloaded and written to the output. :type chunk: bytes """ self.status.chunk_downloaded(len(chunk)) class Status(object): """Holds details about the downland status.""" def __init__(self): self.downloaded = 0 self.total_size = None self.resumed_from = 0 self.time_started = None self.time_finished = None def started(self, resumed_from=0, total_size=None): assert self.time_started is None self.total_size = total_size self.downloaded = self.resumed_from = resumed_from self.time_started = time() def chunk_downloaded(self, size): assert self.time_finished is None self.downloaded += size @property def has_finished(self): return self.time_finished is not None def finished(self): assert self.time_started is not None assert self.time_finished is None self.time_finished = time() class ProgressReporterThread(threading.Thread): """ Reports download progress based on its status. Uses threading to periodically update the status (speed, ETA, etc.). """ def __init__(self, status, output, tick=.1, update_interval=1): """ :type status: Status :type output: file """ super(ProgressReporterThread, self).__init__() self.status = status self.output = output self._tick = tick self._update_interval = update_interval self._spinner_pos = 0 self._status_line = '' self._prev_bytes = 0 self._prev_time = time() self._should_stop = threading.Event() def stop(self): """Stop reporting on next tick.""" self._should_stop.set() def run(self): while not self._should_stop.is_set(): if self.status.has_finished: self.sum_up() break self.report_speed() sleep(self._tick) def report_speed(self): now = time() if now - self._prev_time >= self._update_interval: downloaded = self.status.downloaded try: speed = ((downloaded - self._prev_bytes) / (now - self._prev_time)) except ZeroDivisionError: speed = 0 if not self.status.total_size: self._status_line = PROGRESS_NO_CONTENT_LENGTH.format( downloaded=humanize_bytes(downloaded), speed=humanize_bytes(speed), ) else: try: percentage = downloaded / self.status.total_size * 100 except ZeroDivisionError: percentage = 0 if not speed: eta = '-:--:--' else: s = int((self.status.total_size - downloaded) / speed) h, s = divmod(s, 60 * 60) m, s = divmod(s, 60) eta = '{0}:{1:0>2}:{2:0>2}'.format(h, m, s) self._status_line = PROGRESS.format( percentage=percentage, downloaded=humanize_bytes(downloaded), speed=humanize_bytes(speed), eta=eta, ) self._prev_time = now self._prev_bytes = downloaded self.output.write( CLEAR_LINE + ' ' + SPINNER[self._spinner_pos] + ' ' + self._status_line ) self.output.flush() self._spinner_pos = (self._spinner_pos + 1 if self._spinner_pos + 1 != len(SPINNER) else 0) def sum_up(self): actually_downloaded = ( self.status.downloaded - self.status.resumed_from) time_taken = self.status.time_finished - self.status.time_started self.output.write(CLEAR_LINE) try: speed = actually_downloaded / time_taken except ZeroDivisionError: # Either time is 0 (not all systems provide `time.time` # with a better precision than 1 second), and/or nothing # has been downloaded. speed = actually_downloaded self.output.write(SUMMARY.format( downloaded=humanize_bytes(actually_downloaded), total=(self.status.total_size and humanize_bytes(self.status.total_size)), speed=humanize_bytes(speed), time=time_taken, )) self.output.flush() httpie-0.9.8/httpie/compat.py0000644000000000000000000001412113022157774014664 0ustar rootroot""" Python 2.6, 2.7, and 3.x compatibility. """ import sys is_py2 = sys.version_info[0] == 2 is_py26 = sys.version_info[:2] == (2, 6) is_py27 = sys.version_info[:2] == (2, 7) is_py3 = sys.version_info[0] == 3 is_pypy = 'pypy' in sys.version.lower() is_windows = 'win32' in str(sys.platform).lower() if is_py2: # noinspection PyShadowingBuiltins bytes = str # noinspection PyUnresolvedReferences,PyShadowingBuiltins str = unicode elif is_py3: # noinspection PyShadowingBuiltins str = str # noinspection PyShadowingBuiltins bytes = bytes try: # pragma: no cover # noinspection PyUnresolvedReferences,PyCompatibility from urllib.parse import urlsplit except ImportError: # pragma: no cover # noinspection PyUnresolvedReferences,PyCompatibility from urlparse import urlsplit try: # pragma: no cover # noinspection PyCompatibility from urllib.request import urlopen except ImportError: # pragma: no cover # noinspection PyCompatibility,PyUnresolvedReferences from urllib2 import urlopen try: # pragma: no cover from collections import OrderedDict except ImportError: # pragma: no cover # Python 2.6 OrderedDict class, needed for headers, parameters, etc .### # # noinspection PyCompatibility,PyUnresolvedReferences from UserDict import DictMixin # noinspection PyShadowingBuiltins,PyCompatibility class OrderedDict(dict, DictMixin): # Copyright (c) 2009 Raymond Hettinger # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # noinspection PyMissingConstructor def __init__(self, *args, **kwds): if len(args) > 1: raise TypeError('expected at most 1 arguments, got %d' % len(args)) try: self.__end except AttributeError: self.clear() self.update(*args, **kwds) def clear(self): self.__end = end = [] # noinspection PyUnusedLocal end += [None, end, end] # sentinel node for doubly linked list self.__map = {} # key --> [key, prev, next] dict.clear(self) def __setitem__(self, key, value): if key not in self: end = self.__end curr = end[1] curr[2] = end[1] = self.__map[key] = [key, curr, end] dict.__setitem__(self, key, value) def __delitem__(self, key): dict.__delitem__(self, key) key, prev, next = self.__map.pop(key) prev[2] = next next[1] = prev def __iter__(self): end = self.__end curr = end[2] while curr is not end: yield curr[0] curr = curr[2] def __reversed__(self): end = self.__end curr = end[1] while curr is not end: yield curr[0] curr = curr[1] def popitem(self, last=True): if not self: raise KeyError('dictionary is empty') if last: # noinspection PyUnresolvedReferences key = reversed(self).next() else: key = iter(self).next() value = self.pop(key) return key, value def __reduce__(self): items = [[k, self[k]] for k in self] tmp = self.__map, self.__end del self.__map, self.__end inst_dict = vars(self).copy() self.__map, self.__end = tmp if inst_dict: return self.__class__, (items,), inst_dict return self.__class__, (items,) def keys(self): return list(self) setdefault = DictMixin.setdefault update = DictMixin.update pop = DictMixin.pop values = DictMixin.values items = DictMixin.items iterkeys = DictMixin.iterkeys itervalues = DictMixin.itervalues iteritems = DictMixin.iteritems def __repr__(self): if not self: return '%s()' % (self.__class__.__name__,) return '%s(%r)' % (self.__class__.__name__, self.items()) def copy(self): return self.__class__(self) # noinspection PyMethodOverriding @classmethod def fromkeys(cls, iterable, value=None): d = cls() for key in iterable: d[key] = value return d def __eq__(self, other): if isinstance(other, OrderedDict): if len(self) != len(other): return False for p, q in zip(self.items(), other.items()): if p != q: return False return True return dict.__eq__(self, other) def __ne__(self, other): return not self == other httpie-0.9.8/httpie/__init__.py0000644000000000000000000000115213022157774015140 0ustar rootroot""" HTTPie - a CLI, cURL-like tool for humans. """ __author__ = 'Jakub Roztocil' __version__ = '0.9.8' __licence__ = 'BSD' class ExitStatus: """Exit status code constants.""" OK = 0 ERROR = 1 PLUGIN_ERROR = 7 # 128+2 SIGINT ERROR_CTRL_C = 130 ERROR_TIMEOUT = 2 ERROR_TOO_MANY_REDIRECTS = 6 # Used only when requested with --check-status: ERROR_HTTP_3XX = 3 ERROR_HTTP_4XX = 4 ERROR_HTTP_5XX = 5 EXIT_STATUS_LABELS = dict( (value, key) for key, value in ExitStatus.__dict__.items() if key.isupper() ) httpie-0.9.8/httpie/cli.py0000644000000000000000000004225713022157774014163 0ustar rootroot"""CLI arguments definition. NOTE: the CLI interface may change before reaching v1.0. """ # noinspection PyCompatibility from argparse import ( RawDescriptionHelpFormatter, FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS ) from textwrap import dedent, wrap from httpie import __doc__, __version__ from httpie.input import ( HTTPieArgumentParser, KeyValueArgType, SEP_PROXY, SEP_GROUP_ALL_ITEMS, OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator, readable_file_arg, SSL_VERSION_ARG_MAPPING ) from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE from httpie.plugins import plugin_manager from httpie.plugins.builtin import BuiltinAuthPlugin from httpie.sessions import DEFAULT_SESSIONS_DIR class HTTPieHelpFormatter(RawDescriptionHelpFormatter): """A nicer help formatter. Help for arguments can be indented and contain new lines. It will be de-dented and arguments in the help will be separated by a blank line for better readability. """ def __init__(self, max_help_position=6, *args, **kwargs): # A smaller indent for args help. kwargs['max_help_position'] = max_help_position super(HTTPieHelpFormatter, self).__init__(*args, **kwargs) def _split_lines(self, text, width): text = dedent(text).strip() + '\n\n' return text.splitlines() parser = HTTPieArgumentParser( formatter_class=HTTPieHelpFormatter, description='%s ' % __doc__.strip(), epilog=dedent(""" For every --OPTION there is also a --no-OPTION that reverts OPTION to its default value. Suggestions and bug reports are greatly appreciated: https://github.com/jkbrzt/httpie/issues """), ) ####################################################################### # Positional arguments. ####################################################################### positional = parser.add_argument_group( title='Positional Arguments', description=dedent(""" These arguments come after any flags and in the order they are listed here. Only URL is required. """) ) positional.add_argument( 'method', metavar='METHOD', nargs=OPTIONAL, default=None, help=""" The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...). This argument can be omitted in which case HTTPie will use POST if there is some data to be sent, otherwise GET: $ http example.org # => GET $ http example.org hello=world # => POST """ ) positional.add_argument( 'url', metavar='URL', help=""" The scheme defaults to 'http://' if the URL does not include one. (You can override this with: --default-scheme=https) You can also use a shorthand for localhost $ http :3000 # => http://localhost:3000 $ http :/foo # => http://localhost/foo """ ) positional.add_argument( 'items', metavar='REQUEST_ITEM', nargs=ZERO_OR_MORE, default=None, type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS), help=r""" Optional key-value pairs to be included in the request. The separator used determines the type: ':' HTTP headers: Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0 '==' URL parameters to be appended to the request URI: search==httpie '=' Data fields to be serialized into a JSON object (with --json, -j) or form data (with --form, -f): name=HTTPie language=Python description='CLI HTTP client' ':=' Non-string JSON data fields (only with --json, -j): awesome:=true amount:=42 colors:='["red", "green", "blue"]' '@' Form file fields (only with --form, -f): cs@~/Documents/CV.pdf '=@' A data field like '=', but takes a file path and embeds its content: essay=@Documents/essay.txt ':=@' A raw JSON field like ':=', but takes a file path and embeds its content: package:=@./package.json You can use a backslash to escape a colliding separator in the field name: field-name-with\:colon=value """ ) ####################################################################### # Content type. ####################################################################### content_type = parser.add_argument_group( title='Predefined Content Types', description=None ) content_type.add_argument( '--json', '-j', action='store_true', help=""" (default) Data items from the command line are serialized as a JSON object. The Content-Type and Accept headers are set to application/json (if not specified). """ ) content_type.add_argument( '--form', '-f', action='store_true', help=""" Data items from the command line are serialized as form fields. The Content-Type is set to application/x-www-form-urlencoded (if not specified). The presence of any file fields results in a multipart/form-data request. """ ) ####################################################################### # Output processing ####################################################################### output_processing = parser.add_argument_group(title='Output Processing') output_processing.add_argument( '--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY, choices=sorted(PRETTY_MAP.keys()), help=""" Controls output processing. The value can be "none" to not prettify the output (default for redirected output), "all" to apply both colors and formatting (default for terminal output), "colors", or "format". """ ) output_processing.add_argument( '--style', '-s', dest='style', metavar='STYLE', default=DEFAULT_STYLE, choices=AVAILABLE_STYLES, help=""" Output coloring style (default is "{default}"). One of: {available} For this option to work properly, please make sure that the $TERM environment variable is set to "xterm-256color" or similar (e.g., via `export TERM=xterm-256color' in your ~/.bashrc). """.format( default=DEFAULT_STYLE, available='\n'.join( '{0}{1}'.format(8 * ' ', line.strip()) for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60) ).rstrip(), ) ) ####################################################################### # Output options ####################################################################### output_options = parser.add_argument_group(title='Output Options') output_options.add_argument( '--print', '-p', dest='output_options', metavar='WHAT', help=""" String specifying what the output should contain: '{req_head}' request headers '{req_body}' request body '{res_head}' response headers '{res_body}' response body The default behaviour is '{default}' (i.e., the response headers and body is printed), if standard output is not redirected. If the output is piped to another program or to a file, then only the response body is printed by default. """ .format( req_head=OUT_REQ_HEAD, req_body=OUT_REQ_BODY, res_head=OUT_RESP_HEAD, res_body=OUT_RESP_BODY, default=OUTPUT_OPTIONS_DEFAULT, ) ) output_options.add_argument( '--headers', '-h', dest='output_options', action='store_const', const=OUT_RESP_HEAD, help=""" Print only the response headers. Shortcut for --print={0}. """ .format(OUT_RESP_HEAD) ) output_options.add_argument( '--body', '-b', dest='output_options', action='store_const', const=OUT_RESP_BODY, help=""" Print only the response body. Shortcut for --print={0}. """ .format(OUT_RESP_BODY) ) output_options.add_argument( '--verbose', '-v', dest='verbose', action='store_true', help=""" Verbose output. Print the whole request as well as the response. Also print any intermediary requests/responses (such as redirects). It's a shortcut for: --all --print={0} """ .format(''.join(OUTPUT_OPTIONS)) ) output_options.add_argument( '--all', default=False, action='store_true', help=""" By default, only the final request/response is shown. Use this flag to show any intermediary requests/responses as well. Intermediary requests include followed redirects (with --follow), the first unauthorized request when Digest auth is used (--auth=digest), etc. """ ) output_options.add_argument( '--history-print', '-P', dest='output_options_history', metavar='WHAT', help=""" The same as --print, -p but applies only to intermediary requests/responses (such as redirects) when their inclusion is enabled with --all. If this options is not specified, then they are formatted the same way as the final response. """ ) output_options.add_argument( '--stream', '-S', action='store_true', default=False, help=""" Always stream the output by line, i.e., behave like `tail -f'. Without --stream and with --pretty (either set or implied), HTTPie fetches the whole response before it outputs the processed data. Set this option when you want to continuously display a prettified long-lived response, such as one from the Twitter streaming API. It is useful also without --pretty: It ensures that the output is flushed more often and in smaller chunks. """ ) output_options.add_argument( '--output', '-o', type=FileType('a+b'), dest='output_file', metavar='FILE', help=""" Save output to FILE instead of stdout. If --download is also set, then only the response body is saved to FILE. Other parts of the HTTP exchange are printed to stderr. """ ) output_options.add_argument( '--download', '-d', action='store_true', default=False, help=""" Do not print the response body to stdout. Rather, download it and store it in a file. The filename is guessed unless specified with --output [filename]. This action is similar to the default behaviour of wget. """ ) output_options.add_argument( '--continue', '-c', dest='download_resume', action='store_true', default=False, help=""" Resume an interrupted download. Note that the --output option needs to be specified as well. """ ) ####################################################################### # Sessions ####################################################################### sessions = parser.add_argument_group(title='Sessions')\ .add_mutually_exclusive_group(required=False) session_name_validator = SessionNameValidator( 'Session name contains invalid characters.' ) sessions.add_argument( '--session', metavar='SESSION_NAME_OR_PATH', type=session_name_validator, help=""" Create, or reuse and update a session. Within a session, custom headers, auth credential, as well as any cookies sent by the server persist between requests. Session files are stored in: {session_dir}//.json. """ .format(session_dir=DEFAULT_SESSIONS_DIR) ) sessions.add_argument( '--session-read-only', metavar='SESSION_NAME_OR_PATH', type=session_name_validator, help=""" Create or read a session without updating it form the request/response exchange. """ ) ####################################################################### # Authentication ####################################################################### # ``requests.request`` keyword arguments. auth = parser.add_argument_group(title='Authentication') auth.add_argument( '--auth', '-a', default=None, metavar='USER[:PASS]', help=""" If only the username is provided (-a username), HTTPie will prompt for the password. """, ) class _AuthTypeLazyChoices(object): # Needed for plugin testing def __contains__(self, item): return item in plugin_manager.get_auth_plugin_mapping() def __iter__(self): return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys())) _auth_plugins = plugin_manager.get_auth_plugins() auth.add_argument( '--auth-type', '-A', choices=_AuthTypeLazyChoices(), default=None, help=""" The authentication mechanism to be used. Defaults to "{default}". {types} """ .format(default=_auth_plugins[0].auth_type, types='\n '.join( '"{type}": {name}{package}{description}'.format( type=plugin.auth_type, name=plugin.name, package=( '' if issubclass(plugin, BuiltinAuthPlugin) else ' (provided by %s)' % plugin.package_name ), description=( '' if not plugin.description else '\n ' + ('\n '.join(wrap(plugin.description))) ) ) for plugin in _auth_plugins )), ) ####################################################################### # Network ####################################################################### network = parser.add_argument_group(title='Network') network.add_argument( '--proxy', default=[], action='append', metavar='PROTOCOL:PROXY_URL', type=KeyValueArgType(SEP_PROXY), help=""" String mapping protocol to the URL of the proxy (e.g. http:http://foo.bar:3128). You can specify multiple proxies with different protocols. """ ) network.add_argument( '--follow', '-F', default=False, action='store_true', help=""" Follow 30x Location redirects. """ ) network.add_argument( '--max-redirects', type=int, default=30, help=""" By default, requests have a limit of 30 redirects (works with --follow). """ ) network.add_argument( '--timeout', type=float, default=30, metavar='SECONDS', help=""" The connection timeout of the request in seconds. The default value is 30 seconds. """ ) network.add_argument( '--check-status', default=False, action='store_true', help=""" By default, HTTPie exits with 0 when no network or other fatal errors occur. This flag instructs HTTPie to also check the HTTP status code and exit with an error if the status indicates one. When the server replies with a 4xx (Client Error) or 5xx (Server Error) status code, HTTPie exits with 4 or 5 respectively. If the response is a 3xx (Redirect) and --follow hasn't been set, then the exit status is 3. Also an error message is written to stderr if stdout is redirected. """ ) ####################################################################### # SSL ####################################################################### ssl = parser.add_argument_group(title='SSL') ssl.add_argument( '--verify', default='yes', help=""" Set to "no" to skip checking the host's SSL certificate. You can also pass the path to a CA_BUNDLE file for private certs. You can also set the REQUESTS_CA_BUNDLE environment variable. Defaults to "yes". """ ) ssl.add_argument( '--ssl', # TODO: Maybe something more general, such as --secure-protocol? dest='ssl_version', choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())), help=""" The desired protocol version to use. This will default to SSL v2.3 which will negotiate the highest protocol that both the server and your installation of OpenSSL support. Available protocols may vary depending on OpenSSL installation (only the supported ones are shown here). """ ) ssl.add_argument( '--cert', default=None, type=readable_file_arg, help=""" You can specify a local cert to use as client side SSL certificate. This file may either contain both private key and certificate or you may specify --cert-key separately. """ ) ssl.add_argument( '--cert-key', default=None, type=readable_file_arg, help=""" The private key to use with SSL. Only needed if --cert is given and the certificate file does not contain the private key. """ ) ####################################################################### # Troubleshooting ####################################################################### troubleshooting = parser.add_argument_group(title='Troubleshooting') troubleshooting.add_argument( '--ignore-stdin', '-I', action='store_true', default=False, help=""" Do not attempt to read stdin. """ ) troubleshooting.add_argument( '--help', action='help', default=SUPPRESS, help=""" Show this help message and exit. """ ) troubleshooting.add_argument( '--version', action='version', version=__version__, help=""" Show version and exit. """ ) troubleshooting.add_argument( '--traceback', action='store_true', default=False, help=""" Prints the exception traceback should one occur. """ ) troubleshooting.add_argument( '--default-scheme', default="http", help=""" The default scheme to use if not specified in the URL. """ ) troubleshooting.add_argument( '--debug', action='store_true', default=False, help=""" Prints the exception traceback should one occur, as well as other information useful for debugging HTTPie itself and for reporting bugs. """ ) httpie-0.9.8/httpie/context.py0000644000000000000000000000577013022157774015077 0ustar rootrootimport sys try: import curses except ImportError: curses = None # Compiled w/o curses from httpie.compat import is_windows from httpie.config import DEFAULT_CONFIG_DIR, Config from httpie.utils import repr_dict_nice class Environment(object): """ Information about the execution context (standard streams, config directory, etc). By default, it represents the actual environment. All of the attributes can be overwritten though, which is used by the test suite to simulate various scenarios. """ is_windows = is_windows config_dir = DEFAULT_CONFIG_DIR stdin = sys.stdin stdin_isatty = stdin.isatty() stdin_encoding = None stdout = sys.stdout stdout_isatty = stdout.isatty() stdout_encoding = None stderr = sys.stderr stderr_isatty = stderr.isatty() colors = 256 if not is_windows: if curses: try: curses.setupterm() colors = curses.tigetnum('colors') except curses.error: pass else: # noinspection PyUnresolvedReferences import colorama.initialise stdout = colorama.initialise.wrap_stream( stdout, convert=None, strip=None, autoreset=True, wrap=True ) stderr = colorama.initialise.wrap_stream( stderr, convert=None, strip=None, autoreset=True, wrap=True ) del colorama def __init__(self, **kwargs): """ Use keyword arguments to overwrite any of the class attributes for this instance. """ assert all(hasattr(type(self), attr) for attr in kwargs.keys()) self.__dict__.update(**kwargs) # Keyword arguments > stream.encoding > default utf8 if self.stdin_encoding is None: self.stdin_encoding = getattr( self.stdin, 'encoding', None) or 'utf8' if self.stdout_encoding is None: actual_stdout = self.stdout if is_windows: # noinspection PyUnresolvedReferences from colorama import AnsiToWin32 if isinstance(self.stdout, AnsiToWin32): actual_stdout = self.stdout.wrapped self.stdout_encoding = getattr( actual_stdout, 'encoding', None) or 'utf8' @property def config(self): if not hasattr(self, '_config'): self._config = Config(directory=self.config_dir) if self._config.is_new(): self._config.save() else: self._config.load() return self._config def __str__(self): defaults = dict(type(self).__dict__) actual = dict(defaults) actual.update(self.__dict__) actual['config'] = self.config return repr_dict_nice(dict( (key, value) for key, value in actual.items() if not key.startswith('_')) ) def __repr__(self): return '<{0} {1}>'.format(type(self).__name__, str(self)) httpie-0.9.8/httpie/utils.py0000644000000000000000000000337313022157774014550 0ustar rootrootfrom __future__ import division import json from httpie.compat import is_py26, OrderedDict def load_json_preserve_order(s): if is_py26: return json.loads(s) return json.loads(s, object_pairs_hook=OrderedDict) def repr_dict_nice(d): def prepare_dict(d): for k, v in d.items(): if isinstance(v, dict): v = dict(prepare_dict(v)) elif isinstance(v, bytes): v = v.decode('utf8') elif not isinstance(v, (int, str)): v = repr(v) yield k, v return json.dumps( dict(prepare_dict(d)), indent=4, sort_keys=True, ) def humanize_bytes(n, precision=2): # Author: Doug Latornell # Licence: MIT # URL: http://code.activestate.com/recipes/577081/ """Return a humanized string representation of a number of bytes. Assumes `from __future__ import division`. >>> humanize_bytes(1) '1 B' >>> humanize_bytes(1024, precision=1) '1.0 kB' >>> humanize_bytes(1024 * 123, precision=1) '123.0 kB' >>> humanize_bytes(1024 * 12342, precision=1) '12.1 MB' >>> humanize_bytes(1024 * 12342, precision=2) '12.05 MB' >>> humanize_bytes(1024 * 1234, precision=2) '1.21 MB' >>> humanize_bytes(1024 * 1234 * 1111, precision=2) '1.31 GB' >>> humanize_bytes(1024 * 1234 * 1111, precision=1) '1.3 GB' """ abbrevs = [ (1 << 50, 'PB'), (1 << 40, 'TB'), (1 << 30, 'GB'), (1 << 20, 'MB'), (1 << 10, 'kB'), (1, 'B') ] if n == 1: return '1 B' for factor, suffix in abbrevs: if n >= factor: break # noinspection PyUnboundLocalVariable return '%.*f %s' % (precision, n / factor, suffix) httpie-0.9.8/httpie/output/0000755000000000000000000000000013022157774014370 5ustar rootroothttpie-0.9.8/httpie/output/streams.py0000644000000000000000000002166313022157774016430 0ustar rootrootfrom itertools import chain from functools import partial from httpie.compat import str from httpie.context import Environment from httpie.models import HTTPRequest, HTTPResponse from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_HEAD, OUT_RESP_BODY) from httpie.output.processing import Formatting, Conversion BINARY_SUPPRESSED_NOTICE = ( b'\n' b'+-----------------------------------------+\n' b'| NOTE: binary data not shown in terminal |\n' b'+-----------------------------------------+' ) class BinarySuppressedError(Exception): """An error indicating that the body is binary and won't be written, e.g., for terminal output).""" message = BINARY_SUPPRESSED_NOTICE def write_stream(stream, outfile, flush): """Write the output stream.""" try: # Writing bytes so we use the buffer interface (Python 3). buf = outfile.buffer except AttributeError: buf = outfile for chunk in stream: buf.write(chunk) if flush: outfile.flush() def write_stream_with_colors_win_py3(stream, outfile, flush): """Like `write`, but colorized chunks are written as text directly to `outfile` to ensure it gets processed by colorama. Applies only to Windows with Python 3 and colorized terminal output. """ color = b'\x1b[' encoding = outfile.encoding for chunk in stream: if color in chunk: outfile.write(chunk.decode(encoding)) else: outfile.buffer.write(chunk) if flush: outfile.flush() def build_output_stream(args, env, request, response, output_options): """Build and return a chain of iterators over the `request`-`response` exchange each of which yields `bytes` chunks. """ req_h = OUT_REQ_HEAD in output_options req_b = OUT_REQ_BODY in output_options resp_h = OUT_RESP_HEAD in output_options resp_b = OUT_RESP_BODY in output_options req = req_h or req_b resp = resp_h or resp_b output = [] Stream = get_stream_type(env, args) if req: output.append(Stream( msg=HTTPRequest(request), with_headers=req_h, with_body=req_b)) if req_b and resp: # Request/Response separator. output.append([b'\n\n']) if resp: output.append(Stream( msg=HTTPResponse(response), with_headers=resp_h, with_body=resp_b)) if env.stdout_isatty and resp_b: # Ensure a blank line after the response body. # For terminal output only. output.append([b'\n\n']) return chain(*output) def get_stream_type(env, args): """Pick the right stream type based on `env` and `args`. Wrap it in a partial with the type-specific args so that we don't need to think what stream we are dealing with. """ if not env.stdout_isatty and not args.prettify: Stream = partial( RawStream, chunk_size=RawStream.CHUNK_SIZE_BY_LINE if args.stream else RawStream.CHUNK_SIZE ) elif args.prettify: Stream = partial( PrettyStream if args.stream else BufferedPrettyStream, env=env, conversion=Conversion(), formatting=Formatting( env=env, groups=args.prettify, color_scheme=args.style, explicit_json=args.json, ), ) else: Stream = partial(EncodedStream, env=env) return Stream class BaseStream(object): """Base HTTP message output stream class.""" def __init__(self, msg, with_headers=True, with_body=True, on_body_chunk_downloaded=None): """ :param msg: a :class:`models.HTTPMessage` subclass :param with_headers: if `True`, headers will be included :param with_body: if `True`, body will be included """ assert with_headers or with_body self.msg = msg self.with_headers = with_headers self.with_body = with_body self.on_body_chunk_downloaded = on_body_chunk_downloaded def get_headers(self): """Return the headers' bytes.""" return self.msg.headers.encode('utf8') def iter_body(self): """Return an iterator over the message body.""" raise NotImplementedError() def __iter__(self): """Return an iterator over `self.msg`.""" if self.with_headers: yield self.get_headers() yield b'\r\n\r\n' if self.with_body: try: for chunk in self.iter_body(): yield chunk if self.on_body_chunk_downloaded: self.on_body_chunk_downloaded(chunk) except BinarySuppressedError as e: if self.with_headers: yield b'\n' yield e.message class RawStream(BaseStream): """The message is streamed in chunks with no processing.""" CHUNK_SIZE = 1024 * 100 CHUNK_SIZE_BY_LINE = 1 def __init__(self, chunk_size=CHUNK_SIZE, **kwargs): super(RawStream, self).__init__(**kwargs) self.chunk_size = chunk_size def iter_body(self): return self.msg.iter_body(self.chunk_size) class EncodedStream(BaseStream): """Encoded HTTP message stream. The message bytes are converted to an encoding suitable for `self.env.stdout`. Unicode errors are replaced and binary data is suppressed. The body is always streamed by line. """ CHUNK_SIZE = 1 def __init__(self, env=Environment(), **kwargs): super(EncodedStream, self).__init__(**kwargs) if env.stdout_isatty: # Use the encoding supported by the terminal. output_encoding = env.stdout_encoding else: # Preserve the message encoding. output_encoding = self.msg.encoding # Default to utf8 when unsure. self.output_encoding = output_encoding or 'utf8' def iter_body(self): for line, lf in self.msg.iter_lines(self.CHUNK_SIZE): if b'\0' in line: raise BinarySuppressedError() yield line.decode(self.msg.encoding) \ .encode(self.output_encoding, 'replace') + lf class PrettyStream(EncodedStream): """In addition to :class:`EncodedStream` behaviour, this stream applies content processing. Useful for long-lived HTTP responses that stream by lines such as the Twitter streaming API. """ CHUNK_SIZE = 1 def __init__(self, conversion, formatting, **kwargs): super(PrettyStream, self).__init__(**kwargs) self.formatting = formatting self.conversion = conversion self.mime = self.msg.content_type.split(';')[0] def get_headers(self): return self.formatting.format_headers( self.msg.headers).encode(self.output_encoding) def iter_body(self): first_chunk = True iter_lines = self.msg.iter_lines(self.CHUNK_SIZE) for line, lf in iter_lines: if b'\0' in line: if first_chunk: converter = self.conversion.get_converter(self.mime) if converter: body = bytearray() # noinspection PyAssignmentToLoopOrWithParameter for line, lf in chain([(line, lf)], iter_lines): body.extend(line) body.extend(lf) self.mime, body = converter.convert(body) assert isinstance(body, str) yield self.process_body(body) return raise BinarySuppressedError() yield self.process_body(line) + lf first_chunk = False def process_body(self, chunk): if not isinstance(chunk, str): # Text when a converter has been used, # otherwise it will always be bytes. chunk = chunk.decode(self.msg.encoding, 'replace') chunk = self.formatting.format_body(content=chunk, mime=self.mime) return chunk.encode(self.output_encoding, 'replace') class BufferedPrettyStream(PrettyStream): """The same as :class:`PrettyStream` except that the body is fully fetched before it's processed. Suitable regular HTTP responses. """ CHUNK_SIZE = 1024 * 10 def iter_body(self): # Read the whole body before prettifying it, # but bail out immediately if the body is binary. converter = None body = bytearray() for chunk in self.msg.iter_body(self.CHUNK_SIZE): if not converter and b'\0' in chunk: converter = self.conversion.get_converter(self.mime) if not converter: raise BinarySuppressedError() body.extend(chunk) if converter: self.mime, body = converter.convert(body) yield self.process_body(body) httpie-0.9.8/httpie/output/__init__.py0000644000000000000000000000000013022157774016467 0ustar rootroothttpie-0.9.8/httpie/output/processing.py0000644000000000000000000000265613022157774017127 0ustar rootrootimport re from httpie.plugins import plugin_manager from httpie.context import Environment MIME_RE = re.compile(r'^[^/]+/[^/]+$') def is_valid_mime(mime): return mime and MIME_RE.match(mime) class Conversion(object): def get_converter(self, mime): if is_valid_mime(mime): for converter_class in plugin_manager.get_converters(): if converter_class.supports(mime): return converter_class(mime) class Formatting(object): """A delegate class that invokes the actual processors.""" def __init__(self, groups, env=Environment(), **kwargs): """ :param groups: names of processor groups to be applied :param env: Environment :param kwargs: additional keyword arguments for processors """ available_plugins = plugin_manager.get_formatters_grouped() self.enabled_plugins = [] for group in groups: for cls in available_plugins[group]: p = cls(env=env, **kwargs) if p.enabled: self.enabled_plugins.append(p) def format_headers(self, headers): for p in self.enabled_plugins: headers = p.format_headers(headers) return headers def format_body(self, content, mime): if is_valid_mime(mime): for p in self.enabled_plugins: content = p.format_body(content, mime) return content httpie-0.9.8/httpie/output/formatters/0000755000000000000000000000000013022157774016556 5ustar rootroothttpie-0.9.8/httpie/output/formatters/__init__.py0000644000000000000000000000000013022157774020655 0ustar rootroothttpie-0.9.8/httpie/output/formatters/headers.py0000644000000000000000000000063413022157774020546 0ustar rootrootfrom httpie.plugins import FormatterPlugin class HeadersFormatter(FormatterPlugin): def format_headers(self, headers): """ Sorts headers by name while retaining relative order of multiple headers with the same name. """ lines = headers.splitlines() headers = sorted(lines[1:], key=lambda h: h.split(':')[0]) return '\r\n'.join(lines[:1] + headers) httpie-0.9.8/httpie/output/formatters/colors.py0000644000000000000000000001706613022157774020443 0ustar rootrootfrom __future__ import absolute_import import json import pygments.lexer import pygments.token import pygments.styles import pygments.lexers import pygments.style from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal256 import Terminal256Formatter from pygments.lexers.special import TextLexer from pygments.util import ClassNotFound from httpie.compat import is_windows from httpie.plugins import FormatterPlugin AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys()) AVAILABLE_STYLES.add('solarized') if is_windows: # Colors on Windows via colorama don't look that # great and fruity seems to give the best result there DEFAULT_STYLE = 'fruity' else: DEFAULT_STYLE = 'solarized' class ColorFormatter(FormatterPlugin): """ Colorize using Pygments This processor that applies syntax highlighting to the headers, and also to the body if its content type is recognized. """ group_name = 'colors' def __init__(self, env, explicit_json=False, color_scheme=DEFAULT_STYLE, **kwargs): super(ColorFormatter, self).__init__(**kwargs) if not env.colors: self.enabled = False return # --json, -j self.explicit_json = explicit_json try: style_class = pygments.styles.get_style_by_name(color_scheme) except ClassNotFound: style_class = Solarized256Style if env.colors == 256: fmt_class = Terminal256Formatter else: fmt_class = TerminalFormatter self.formatter = fmt_class(style=style_class) def format_headers(self, headers): return pygments.highlight(headers, HTTPLexer(), self.formatter).strip() def format_body(self, body, mime): lexer = self.get_lexer(mime, body) if lexer: body = pygments.highlight(body, lexer, self.formatter) return body.strip() def get_lexer(self, mime, body): return get_lexer( mime=mime, explicit_json=self.explicit_json, body=body, ) def get_lexer(mime, explicit_json=False, body=''): # Build candidate mime type and lexer names. mime_types, lexer_names = [mime], [] type_, subtype = mime.split('/', 1) if '+' not in subtype: lexer_names.append(subtype) else: subtype_name, subtype_suffix = subtype.split('+', 1) lexer_names.extend([subtype_name, subtype_suffix]) mime_types.extend([ '%s/%s' % (type_, subtype_name), '%s/%s' % (type_, subtype_suffix) ]) # As a last resort, if no lexer feels responsible, and # the subtype contains 'json', take the JSON lexer if 'json' in subtype: lexer_names.append('json') # Try to resolve the right lexer. lexer = None for mime_type in mime_types: try: lexer = pygments.lexers.get_lexer_for_mimetype(mime_type) break except ClassNotFound: pass else: for name in lexer_names: try: lexer = pygments.lexers.get_lexer_by_name(name) except ClassNotFound: pass if explicit_json and body and (not lexer or isinstance(lexer, TextLexer)): # JSON response with an incorrect Content-Type? try: json.loads(body) # FIXME: the body also gets parsed in json.py except ValueError: pass # Nope else: lexer = pygments.lexers.get_lexer_by_name('json') return lexer class HTTPLexer(pygments.lexer.RegexLexer): """Simplified HTTP lexer for Pygments. It only operates on headers and provides a stronger contrast between their names and values than the original one bundled with Pygments (:class:`pygments.lexers.text import HttpLexer`), especially when Solarized color scheme is used. """ name = 'HTTP' aliases = ['http'] filenames = ['*.http'] tokens = { 'root': [ # Request-Line (r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)', pygments.lexer.bygroups( pygments.token.Name.Function, pygments.token.Text, pygments.token.Name.Namespace, pygments.token.Text, pygments.token.Keyword.Reserved, pygments.token.Operator, pygments.token.Number )), # Response Status-Line (r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)', pygments.lexer.bygroups( pygments.token.Keyword.Reserved, # 'HTTP' pygments.token.Operator, # '/' pygments.token.Number, # Version pygments.token.Text, pygments.token.Number, # Status code pygments.token.Text, pygments.token.Name.Exception, # Reason )), # Header (r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups( pygments.token.Name.Attribute, # Name pygments.token.Text, pygments.token.Operator, # Colon pygments.token.Text, pygments.token.String # Value )) ] } class Solarized256Style(pygments.style.Style): """ solarized256 ------------ A Pygments style inspired by Solarized's 256 color mode. :copyright: (c) 2011 by Hank Gay, (c) 2012 by John Mastro. :license: BSD, see LICENSE for more details. """ BASE03 = "#1c1c1c" BASE02 = "#262626" BASE01 = "#4e4e4e" BASE00 = "#585858" BASE0 = "#808080" BASE1 = "#8a8a8a" BASE2 = "#d7d7af" BASE3 = "#ffffd7" YELLOW = "#af8700" ORANGE = "#d75f00" RED = "#af0000" MAGENTA = "#af005f" VIOLET = "#5f5faf" BLUE = "#0087ff" CYAN = "#00afaf" GREEN = "#5f8700" background_color = BASE03 styles = { pygments.token.Keyword: GREEN, pygments.token.Keyword.Constant: ORANGE, pygments.token.Keyword.Declaration: BLUE, pygments.token.Keyword.Namespace: ORANGE, pygments.token.Keyword.Reserved: BLUE, pygments.token.Keyword.Type: RED, pygments.token.Name.Attribute: BASE1, pygments.token.Name.Builtin: BLUE, pygments.token.Name.Builtin.Pseudo: BLUE, pygments.token.Name.Class: BLUE, pygments.token.Name.Constant: ORANGE, pygments.token.Name.Decorator: BLUE, pygments.token.Name.Entity: ORANGE, pygments.token.Name.Exception: YELLOW, pygments.token.Name.Function: BLUE, pygments.token.Name.Tag: BLUE, pygments.token.Name.Variable: BLUE, pygments.token.String: CYAN, pygments.token.String.Backtick: BASE01, pygments.token.String.Char: CYAN, pygments.token.String.Doc: CYAN, pygments.token.String.Escape: RED, pygments.token.String.Heredoc: CYAN, pygments.token.String.Regex: RED, pygments.token.Number: CYAN, pygments.token.Operator: BASE1, pygments.token.Operator.Word: GREEN, pygments.token.Comment: BASE01, pygments.token.Comment.Preproc: GREEN, pygments.token.Comment.Special: GREEN, pygments.token.Generic.Deleted: CYAN, pygments.token.Generic.Emph: 'italic', pygments.token.Generic.Error: RED, pygments.token.Generic.Heading: ORANGE, pygments.token.Generic.Inserted: GREEN, pygments.token.Generic.Strong: 'bold', pygments.token.Generic.Subheading: ORANGE, pygments.token.Token: BASE1, pygments.token.Token.Other: ORANGE, } httpie-0.9.8/httpie/output/formatters/json.py0000644000000000000000000000156413022157774020107 0ustar rootrootfrom __future__ import absolute_import import json from httpie.plugins import FormatterPlugin DEFAULT_INDENT = 4 class JSONFormatter(FormatterPlugin): def format_body(self, body, mime): maybe_json = [ 'json', 'javascript', 'text', ] if (self.kwargs['explicit_json'] or any(token in mime for token in maybe_json)): try: obj = json.loads(body) except ValueError: pass # Invalid JSON, ignore. else: # Indent, sort keys by name, and avoid # unicode escapes to improve readability. body = json.dumps( obj=obj, sort_keys=True, ensure_ascii=False, indent=DEFAULT_INDENT ) return body httpie-0.9.8/httpie/config.py0000644000000000000000000000557313022157774014661 0ustar rootrootimport os import json import errno from httpie import __version__ from httpie.compat import is_windows DEFAULT_CONFIG_DIR = str(os.environ.get( 'HTTPIE_CONFIG_DIR', os.path.expanduser('~/.httpie') if not is_windows else os.path.expandvars(r'%APPDATA%\\httpie') )) class BaseConfigDict(dict): name = None helpurl = None about = None def __getattr__(self, item): return self[item] def _get_path(self): """Return the config file path without side-effects.""" raise NotImplementedError() @property def path(self): """Return the config file path creating basedir, if needed.""" path = self._get_path() try: os.makedirs(os.path.dirname(path), mode=0o700) except OSError as e: if e.errno != errno.EEXIST: raise return path def is_new(self): return not os.path.exists(self._get_path()) def load(self): try: with open(self.path, 'rt') as f: try: data = json.load(f) except ValueError as e: raise ValueError( 'Invalid %s JSON: %s [%s]' % (type(self).__name__, str(e), self.path) ) self.update(data) except IOError as e: if e.errno != errno.ENOENT: raise def save(self): self['__meta__'] = { 'httpie': __version__ } if self.helpurl: self['__meta__']['help'] = self.helpurl if self.about: self['__meta__']['about'] = self.about with open(self.path, 'w') as f: json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True) f.write('\n') def delete(self): try: os.unlink(self.path) except OSError as e: if e.errno != errno.ENOENT: raise class Config(BaseConfigDict): name = 'config' helpurl = 'https://httpie.org/docs#config' about = 'HTTPie configuration file' DEFAULTS = { 'default_options': [] } def __init__(self, directory=DEFAULT_CONFIG_DIR): super(Config, self).__init__() self.update(self.DEFAULTS) self.directory = directory def load(self): super(Config, self).load() self._migrate_implicit_content_type() def _get_path(self): return os.path.join(self.directory, self.name + '.json') def _migrate_implicit_content_type(self): """Migrate the removed implicit_content_type config option""" try: implicit_content_type = self.pop('implicit_content_type') except KeyError: pass else: if implicit_content_type == 'form': self['default_options'].insert(0, '--form') self.save() self.load() httpie-0.9.8/httpie/__main__.py0000644000000000000000000000050713022157774015124 0ustar rootroot#!/usr/bin/env python """The main entry point. Invoke as `http' or `python -m httpie'. """ import sys def main(): try: from .core import main sys.exit(main()) except KeyboardInterrupt: from . import ExitStatus sys.exit(ExitStatus.ERROR_CTRL_C) if __name__ == '__main__': main() httpie-0.9.8/httpie/sessions.py0000644000000000000000000001233313022157774015252 0ustar rootroot"""Persistent, JSON-serialized sessions. """ import re import os from requests.cookies import RequestsCookieJar, create_cookie from httpie.compat import urlsplit from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR from httpie.plugins import plugin_manager SESSIONS_DIR_NAME = 'sessions' DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME) VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$') # Request headers starting with these prefixes won't be stored in sessions. # They are specific to each request. # http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] def get_response(requests_session, session_name, config_dir, args, read_only=False): """Like `client.get_responses`, but applies permanent aspects of the session to the request. """ from .client import get_requests_kwargs, dump_request if os.path.sep in session_name: path = os.path.expanduser(session_name) else: hostname = (args.headers.get('Host', None) or urlsplit(args.url).netloc.split('@')[-1]) if not hostname: # HACK/FIXME: httpie-unixsocket's URLs have no hostname. hostname = 'localhost' # host:port => host_port hostname = hostname.replace(':', '_') path = os.path.join(config_dir, SESSIONS_DIR_NAME, hostname, session_name + '.json') session = Session(path) session.load() kwargs = get_requests_kwargs(args, base_headers=session.headers) if args.debug: dump_request(kwargs) session.update_headers(kwargs['headers']) if args.auth_plugin: session.auth = { 'type': args.auth_plugin.auth_type, 'raw_auth': args.auth_plugin.raw_auth, } elif session.auth: kwargs['auth'] = session.auth requests_session.cookies = session.cookies try: response = requests_session.request(**kwargs) except Exception: raise else: # Existing sessions with `read_only=True` don't get updated. if session.is_new() or not read_only: session.cookies = requests_session.cookies session.save() return response class Session(BaseConfigDict): helpurl = 'https://httpie.org/docs#sessions' about = 'HTTPie session file' def __init__(self, path, *args, **kwargs): super(Session, self).__init__(*args, **kwargs) self._path = path self['headers'] = {} self['cookies'] = {} self['auth'] = { 'type': None, 'username': None, 'password': None } def _get_path(self): return self._path def update_headers(self, request_headers): """ Update the session headers with the request ones while ignoring certain name prefixes. :type request_headers: dict """ for name, value in request_headers.items(): if value is None: continue # Ignore explicitely unset headers value = value.decode('utf8') if name == 'User-Agent' and value.startswith('HTTPie/'): continue for prefix in SESSION_IGNORED_HEADER_PREFIXES: if name.lower().startswith(prefix.lower()): break else: self['headers'][name] = value @property def headers(self): return self['headers'] @property def cookies(self): jar = RequestsCookieJar() for name, cookie_dict in self['cookies'].items(): jar.set_cookie(create_cookie( name, cookie_dict.pop('value'), **cookie_dict)) jar.clear_expired_cookies() return jar @cookies.setter def cookies(self, jar): """ :type jar: CookieJar """ # http://docs.python.org/2/library/cookielib.html#cookie-objects stored_attrs = ['value', 'path', 'secure', 'expires'] self['cookies'] = {} for cookie in jar: self['cookies'][cookie.name] = dict( (attname, getattr(cookie, attname)) for attname in stored_attrs ) @property def auth(self): auth = self.get('auth', None) if not auth or not auth['type']: return plugin = plugin_manager.get_auth_plugin(auth['type'])() credentials = {'username': None, 'password': None} try: # New style plugin.raw_auth = auth['raw_auth'] except KeyError: # Old style credentials = { 'username': auth['username'], 'password': auth['password'], } else: if plugin.auth_parse: from httpie.input import parse_auth parsed = parse_auth(plugin.raw_auth) credentials = { 'username': parsed.key, 'password': parsed.value, } return plugin.get_auth(**credentials) @auth.setter def auth(self, auth): assert set(['type', 'raw_auth']) == set(auth.keys()) self['auth'] = auth httpie-0.9.8/httpie/core.py0000644000000000000000000002027013022157774014333 0ustar rootroot"""This module provides the main functionality of HTTPie. Invocation flow: 1. Read, validate and process the input (args, `stdin`). 2. Create and send a request. 3. Stream, and possibly process and format, the parts of the request-response exchange selected by output options. 4. Simultaneously write to `stdout` 5. Exit. """ import sys import errno import platform import requests from requests import __version__ as requests_version from pygments import __version__ as pygments_version from httpie import __version__ as httpie_version, ExitStatus from httpie.compat import str, bytes, is_py3 from httpie.client import get_response from httpie.downloads import Downloader from httpie.context import Environment from httpie.plugins import plugin_manager from httpie.output.streams import ( build_output_stream, write_stream, write_stream_with_colors_win_py3 ) def get_exit_status(http_status, follow=False): """Translate HTTP status code to exit status code.""" if 300 <= http_status <= 399 and not follow: # Redirect return ExitStatus.ERROR_HTTP_3XX elif 400 <= http_status <= 499: # Client Error return ExitStatus.ERROR_HTTP_4XX elif 500 <= http_status <= 599: # Server Error return ExitStatus.ERROR_HTTP_5XX else: return ExitStatus.OK def print_debug_info(env): env.stderr.writelines([ 'HTTPie %s\n' % httpie_version, 'Requests %s\n' % requests_version, 'Pygments %s\n' % pygments_version, 'Python %s\n%s\n' % (sys.version, sys.executable), '%s %s' % (platform.system(), platform.release()), ]) env.stderr.write('\n\n') env.stderr.write(repr(env)) env.stderr.write('\n') def decode_args(args, stdin_encoding): """ Convert all bytes ags to str by decoding them using stdin encoding. """ return [ arg.decode(stdin_encoding) if type(arg) == bytes else arg for arg in args ] def program(args, env, log_error): """ The main program without error handling :param args: parsed args (argparse.Namespace) :type env: Environment :param log_error: error log function :return: status code """ exit_status = ExitStatus.OK downloader = None show_traceback = args.debug or args.traceback try: if args.download: args.follow = True # --download implies --follow. downloader = Downloader( output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume ) downloader.pre_request(args.headers) final_response = get_response(args, config_dir=env.config.directory) if args.all: responses = final_response.history + [final_response] else: responses = [final_response] for response in responses: if args.check_status or downloader: exit_status = get_exit_status( http_status=response.status_code, follow=args.follow ) if not env.stdout_isatty and exit_status != ExitStatus.OK: log_error( 'HTTP %s %s', response.raw.status, response.raw.reason, level='warning' ) write_stream_kwargs = { 'stream': build_output_stream( args=args, env=env, request=response.request, response=response, output_options=( args.output_options if response is final_response else args.output_options_history ) ), # NOTE: `env.stdout` will in fact be `stderr` with `--download` 'outfile': env.stdout, 'flush': env.stdout_isatty or args.stream } try: if env.is_windows and is_py3 and 'colors' in args.prettify: write_stream_with_colors_win_py3(**write_stream_kwargs) else: write_stream(**write_stream_kwargs) except IOError as e: if not show_traceback and e.errno == errno.EPIPE: # Ignore broken pipes unless --traceback. env.stderr.write('\n') else: raise if downloader and exit_status == ExitStatus.OK: # Last response body download. download_stream, download_to = downloader.start(final_response) write_stream( stream=download_stream, outfile=download_to, flush=False, ) downloader.finish() if downloader.interrupted: exit_status = ExitStatus.ERROR log_error('Incomplete download: size=%d; downloaded=%d' % ( downloader.status.total_size, downloader.status.downloaded )) return exit_status finally: if downloader and not downloader.finished: downloader.failed() if (not isinstance(args, list) and args.output_file and args.output_file_specified): args.output_file.close() def main(args=sys.argv[1:], env=Environment(), custom_log_error=None): """ The main function. Pre-process args, handle some special types of invocations, and run the main program with error handling. Return exit status code. """ args = decode_args(args, env.stdin_encoding) plugin_manager.load_installed_plugins() def log_error(msg, *args, **kwargs): msg = msg % args level = kwargs.get('level', 'error') assert level in ['error', 'warning'] env.stderr.write('\nhttp: %s: %s\n' % (level, msg)) from httpie.cli import parser if env.config.default_options: args = env.config.default_options + args if custom_log_error: log_error = custom_log_error include_debug_info = '--debug' in args include_traceback = include_debug_info or '--traceback' in args if include_debug_info: print_debug_info(env) if args == ['--debug']: return ExitStatus.OK exit_status = ExitStatus.OK try: parsed_args = parser.parse_args(args=args, env=env) except KeyboardInterrupt: env.stderr.write('\n') if include_traceback: raise exit_status = ExitStatus.ERROR_CTRL_C except SystemExit as e: if e.code != ExitStatus.OK: env.stderr.write('\n') if include_traceback: raise exit_status = ExitStatus.ERROR else: try: exit_status = program( args=parsed_args, env=env, log_error=log_error, ) except KeyboardInterrupt: env.stderr.write('\n') if include_traceback: raise exit_status = ExitStatus.ERROR_CTRL_C except SystemExit as e: if e.code != ExitStatus.OK: env.stderr.write('\n') if include_traceback: raise exit_status = ExitStatus.ERROR except requests.Timeout: exit_status = ExitStatus.ERROR_TIMEOUT log_error('Request timed out (%ss).', parsed_args.timeout) except requests.TooManyRedirects: exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS log_error('Too many redirects (--max-redirects=%s).', parsed_args.max_redirects) except Exception as e: # TODO: Further distinction between expected and unexpected errors. msg = str(e) if hasattr(e, 'request'): request = e.request if hasattr(request, 'url'): msg += ' while doing %s request to URL: %s' % ( request.method, request.url) log_error('%s: %s', type(e).__name__, msg) if include_traceback: raise exit_status = ExitStatus.ERROR return exit_status httpie-0.9.8/httpie/models.py0000644000000000000000000000762313022157774014675 0ustar rootrootfrom httpie.compat import urlsplit, str class HTTPMessage(object): """Abstract class for HTTP messages.""" def __init__(self, orig): self._orig = orig def iter_body(self, chunk_size): """Return an iterator over the body.""" raise NotImplementedError() def iter_lines(self, chunk_size): """Return an iterator over the body yielding (`line`, `line_feed`).""" raise NotImplementedError() @property def headers(self): """Return a `str` with the message's headers.""" raise NotImplementedError() @property def encoding(self): """Return a `str` with the message's encoding, if known.""" raise NotImplementedError() @property def body(self): """Return a `bytes` with the message's body.""" raise NotImplementedError() @property def content_type(self): """Return the message content type.""" ct = self._orig.headers.get('Content-Type', '') if not isinstance(ct, str): ct = ct.decode('utf8') return ct class HTTPResponse(HTTPMessage): """A :class:`requests.models.Response` wrapper.""" def iter_body(self, chunk_size=1): return self._orig.iter_content(chunk_size=chunk_size) def iter_lines(self, chunk_size): return ((line, b'\n') for line in self._orig.iter_lines(chunk_size)) # noinspection PyProtectedMember @property def headers(self): original = self._orig.raw._original_response version = { 9: '0.9', 10: '1.0', 11: '1.1', 20: '2', }[original.version] status_line = 'HTTP/{version} {status} {reason}'.format( version=version, status=original.status, reason=original.reason ) headers = [status_line] try: # `original.msg` is a `http.client.HTTPMessage` on Python 3 # `_headers` is a 2-tuple headers.extend( '%s: %s' % header for header in original.msg._headers) except AttributeError: # and a `httplib.HTTPMessage` on Python 2.x # `headers` is a list of `name: val`. headers.extend(h.strip() for h in original.msg.headers) return '\r\n'.join(headers) @property def encoding(self): return self._orig.encoding or 'utf8' @property def body(self): # Only now the response body is fetched. # Shouldn't be touched unless the body is actually needed. return self._orig.content class HTTPRequest(HTTPMessage): """A :class:`requests.models.Request` wrapper.""" def iter_body(self, chunk_size): yield self.body def iter_lines(self, chunk_size): yield self.body, b'' @property def headers(self): url = urlsplit(self._orig.url) request_line = '{method} {path}{query} HTTP/1.1'.format( method=self._orig.method, path=url.path or '/', query='?' + url.query if url.query else '' ) headers = dict(self._orig.headers) if 'Host' not in self._orig.headers: headers['Host'] = url.netloc.split('@')[-1] headers = [ '%s: %s' % ( name, value if isinstance(value, str) else value.decode('utf8') ) for name, value in headers.items() ] headers.insert(0, request_line) headers = '\r\n'.join(headers).strip() if isinstance(headers, bytes): # Python < 3 headers = headers.decode('utf8') return headers @property def encoding(self): return 'utf8' @property def body(self): body = self._orig.body if isinstance(body, str): # Happens with JSON/form request data parsed from the command line. body = body.encode('utf8') return body or b'' httpie-0.9.8/httpie/input.py0000644000000000000000000006016413022157774014550 0ustar rootroot"""Parsing and processing of CLI input (args, auth credentials, files, stdin). """ import os import ssl import sys import re import errno import mimetypes import getpass from io import BytesIO from collections import namedtuple, Iterable # noinspection PyCompatibility from argparse import ArgumentParser, ArgumentTypeError, ArgumentError # TODO: Use MultiDict for headers once added to `requests`. # https://github.com/jkbrzt/httpie/issues/130 from httpie.plugins import plugin_manager from requests.structures import CaseInsensitiveDict from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27 from httpie.sessions import VALID_SESSION_NAME_PATTERN from httpie.utils import load_json_preserve_order # ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) # URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE) HTTP_POST = 'POST' HTTP_GET = 'GET' # Various separators used in args SEP_HEADERS = ':' SEP_HEADERS_EMPTY = ';' SEP_CREDENTIALS = ':' SEP_PROXY = ':' SEP_DATA = '=' SEP_DATA_RAW_JSON = ':=' SEP_FILES = '@' SEP_DATA_EMBED_FILE = '=@' SEP_DATA_EMBED_RAW_JSON_FILE = ':=@' SEP_QUERY = '==' # Separators that become request data SEP_GROUP_DATA_ITEMS = frozenset([ SEP_DATA, SEP_DATA_RAW_JSON, SEP_FILES, SEP_DATA_EMBED_FILE, SEP_DATA_EMBED_RAW_JSON_FILE ]) # Separators for items whose value is a filename to be embedded SEP_GROUP_DATA_EMBED_ITEMS = frozenset([ SEP_DATA_EMBED_FILE, SEP_DATA_EMBED_RAW_JSON_FILE, ]) # Separators for raw JSON items SEP_GROUP_RAW_JSON_ITEMS = frozenset([ SEP_DATA_RAW_JSON, SEP_DATA_EMBED_RAW_JSON_FILE, ]) # Separators allowed in ITEM arguments SEP_GROUP_ALL_ITEMS = frozenset([ SEP_HEADERS, SEP_HEADERS_EMPTY, SEP_QUERY, SEP_DATA, SEP_DATA_RAW_JSON, SEP_FILES, SEP_DATA_EMBED_FILE, SEP_DATA_EMBED_RAW_JSON_FILE, ]) # Output options OUT_REQ_HEAD = 'H' OUT_REQ_BODY = 'B' OUT_RESP_HEAD = 'h' OUT_RESP_BODY = 'b' OUTPUT_OPTIONS = frozenset([ OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, OUT_RESP_BODY ]) # Pretty PRETTY_MAP = { 'all': ['format', 'colors'], 'colors': ['colors'], 'format': ['format'], 'none': [] } PRETTY_STDOUT_TTY_ONLY = object() # Defaults OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY SSL_VERSION_ARG_MAPPING = { 'ssl2.3': 'PROTOCOL_SSLv23', 'ssl3': 'PROTOCOL_SSLv3', 'tls1': 'PROTOCOL_TLSv1', 'tls1.1': 'PROTOCOL_TLSv1_1', 'tls1.2': 'PROTOCOL_TLSv1_2', } SSL_VERSION_ARG_MAPPING = dict( (cli_arg, getattr(ssl, ssl_constant)) for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items() if hasattr(ssl, ssl_constant) ) class HTTPieArgumentParser(ArgumentParser): """Adds additional logic to `argparse.ArgumentParser`. Handles all input (CLI args, file args, stdin), applies defaults, and performs extra validation. """ def __init__(self, *args, **kwargs): kwargs['add_help'] = False super(HTTPieArgumentParser, self).__init__(*args, **kwargs) # noinspection PyMethodOverriding def parse_args(self, env, args=None, namespace=None): self.env = env self.args, no_options = super(HTTPieArgumentParser, self)\ .parse_known_args(args, namespace) if self.args.debug: self.args.traceback = True # Arguments processing and environment setup. self._apply_no_options(no_options) self._validate_download_options() self._setup_standard_streams() self._process_output_options() self._process_pretty_options() self._guess_method() self._parse_items() if not self.args.ignore_stdin and not env.stdin_isatty: self._body_from_file(self.env.stdin) if not URL_SCHEME_RE.match(self.args.url): scheme = self.args.default_scheme + "://" # See if we're using curl style shorthand for localhost (:3000/foo) shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url) if shorthand: port = shorthand.group(1) rest = shorthand.group(2) self.args.url = scheme + 'localhost' if port: self.args.url += ':' + port self.args.url += rest else: self.args.url = scheme + self.args.url self._process_auth() return self.args # noinspection PyShadowingBuiltins def _print_message(self, message, file=None): # Sneak in our stderr/stdout. file = { sys.stdout: self.env.stdout, sys.stderr: self.env.stderr, None: self.env.stderr }.get(file, file) if not hasattr(file, 'buffer') and isinstance(message, str): message = message.encode(self.env.stdout_encoding) super(HTTPieArgumentParser, self)._print_message(message, file) def _setup_standard_streams(self): """ Modify `env.stdout` and `env.stdout_isatty` based on args, if needed. """ self.args.output_file_specified = bool(self.args.output_file) if self.args.download: # FIXME: Come up with a cleaner solution. if not self.args.output_file and not self.env.stdout_isatty: # Use stdout as the download output file. self.args.output_file = self.env.stdout # With `--download`, we write everything that would normally go to # `stdout` to `stderr` instead. Let's replace the stream so that # we don't have to use many `if`s throughout the codebase. # The response body will be treated separately. self.env.stdout = self.env.stderr self.env.stdout_isatty = self.env.stderr_isatty elif self.args.output_file: # When not `--download`ing, then `--output` simply replaces # `stdout`. The file is opened for appending, which isn't what # we want in this case. self.args.output_file.seek(0) try: self.args.output_file.truncate() except IOError as e: if e.errno == errno.EINVAL: # E.g. /dev/null on Linux. pass else: raise self.env.stdout = self.args.output_file self.env.stdout_isatty = False def _process_auth(self): # TODO: refactor self.args.auth_plugin = None default_auth_plugin = plugin_manager.get_auth_plugins()[0] auth_type_set = self.args.auth_type is not None url = urlsplit(self.args.url) if self.args.auth is None and not auth_type_set: if url.username is not None: # Handle http://username:password@hostname/ username = url.username password = url.password or '' self.args.auth = AuthCredentials( key=username, value=password, sep=SEP_CREDENTIALS, orig=SEP_CREDENTIALS.join([username, password]) ) if self.args.auth is not None or auth_type_set: if not self.args.auth_type: self.args.auth_type = default_auth_plugin.auth_type plugin = plugin_manager.get_auth_plugin(self.args.auth_type)() if plugin.auth_require and self.args.auth is None: self.error('--auth required') plugin.raw_auth = self.args.auth self.args.auth_plugin = plugin already_parsed = isinstance(self.args.auth, AuthCredentials) if self.args.auth is None or not plugin.auth_parse: self.args.auth = plugin.get_auth() else: if already_parsed: # from the URL credentials = self.args.auth else: credentials = parse_auth(self.args.auth) if (not credentials.has_password() and plugin.prompt_password): if self.args.ignore_stdin: # Non-tty stdin read by now self.error( 'Unable to prompt for passwords because' ' --ignore-stdin is set.' ) credentials.prompt_password(url.netloc) self.args.auth = plugin.get_auth( username=credentials.key, password=credentials.value, ) def _apply_no_options(self, no_options): """For every `--no-OPTION` in `no_options`, set `args.OPTION` to its default value. This allows for un-setting of options, e.g., specified in config. """ invalid = [] for option in no_options: if not option.startswith('--no-'): invalid.append(option) continue # --no-option => --option inverted = '--' + option[5:] for action in self._actions: if inverted in action.option_strings: setattr(self.args, action.dest, action.default) break else: invalid.append(option) if invalid: msg = 'unrecognized arguments: %s' self.error(msg % ' '.join(invalid)) def _body_from_file(self, fd): """There can only be one source of request data. Bytes are always read. """ if self.args.data: self.error('Request body (from stdin or a file) and request ' 'data (key=value) cannot be mixed.') self.args.data = getattr(fd, 'buffer', fd).read() def _guess_method(self): """Set `args.method` if not specified to either POST or GET based on whether the request has data or not. """ if self.args.method is None: # Invoked as `http URL'. assert not self.args.items if not self.args.ignore_stdin and not self.env.stdin_isatty: self.args.method = HTTP_POST else: self.args.method = HTTP_GET # FIXME: False positive, e.g., "localhost" matches but is a valid URL. elif not re.match('^[a-zA-Z]+$', self.args.method): # Invoked as `http URL item+'. The URL is now in `args.method` # and the first ITEM is now incorrectly in `args.url`. try: # Parse the URL as an ITEM and store it as the first ITEM arg. self.args.items.insert(0, KeyValueArgType( *SEP_GROUP_ALL_ITEMS).__call__(self.args.url)) except ArgumentTypeError as e: if self.args.traceback: raise self.error(e.args[0]) else: # Set the URL correctly self.args.url = self.args.method # Infer the method has_data = ( (not self.args.ignore_stdin and not self.env.stdin_isatty) or any(item.sep in SEP_GROUP_DATA_ITEMS for item in self.args.items) ) self.args.method = HTTP_POST if has_data else HTTP_GET def _parse_items(self): """Parse `args.items` into `args.headers`, `args.data`, `args.params`, and `args.files`. """ try: items = parse_items( items=self.args.items, data_class=ParamsDict if self.args.form else OrderedDict ) except ParseError as e: if self.args.traceback: raise self.error(e.args[0]) else: self.args.headers = items.headers self.args.data = items.data self.args.files = items.files self.args.params = items.params if self.args.files and not self.args.form: # `http url @/path/to/file` file_fields = list(self.args.files.keys()) if file_fields != ['']: self.error( 'Invalid file fields (perhaps you meant --form?): %s' % ','.join(file_fields)) fn, fd, ct = self.args.files[''] self.args.files = {} self._body_from_file(fd) if 'Content-Type' not in self.args.headers: content_type = get_content_type(fn) if content_type: self.args.headers['Content-Type'] = content_type def _process_output_options(self): """Apply defaults to output options, or validate the provided ones. The default output options are stdout-type-sensitive. """ def check_options(value, option): unknown = set(value) - OUTPUT_OPTIONS if unknown: self.error('Unknown output options: {0}={1}'.format( option, ','.join(unknown) )) if self.args.verbose: self.args.all = True if self.args.output_options is None: if self.args.verbose: self.args.output_options = ''.join(OUTPUT_OPTIONS) else: self.args.output_options = ( OUTPUT_OPTIONS_DEFAULT if self.env.stdout_isatty else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED ) if self.args.output_options_history is None: self.args.output_options_history = self.args.output_options check_options(self.args.output_options, '--print') check_options(self.args.output_options_history, '--history-print') if self.args.download and OUT_RESP_BODY in self.args.output_options: # Response body is always downloaded with --download and it goes # through a different routine, so we remove it. self.args.output_options = str( set(self.args.output_options) - set(OUT_RESP_BODY)) def _process_pretty_options(self): if self.args.prettify == PRETTY_STDOUT_TTY_ONLY: self.args.prettify = PRETTY_MAP[ 'all' if self.env.stdout_isatty else 'none'] elif (self.args.prettify and self.env.is_windows and self.args.output_file): self.error('Only terminal output can be colorized on Windows.') else: # noinspection PyTypeChecker self.args.prettify = PRETTY_MAP[self.args.prettify] def _validate_download_options(self): if not self.args.download: if self.args.download_resume: self.error('--continue only works with --download') if self.args.download_resume and not ( self.args.download and self.args.output_file): self.error('--continue requires --output to be specified') class ParseError(Exception): pass class KeyValue(object): """Base key-value pair parsed from CLI.""" def __init__(self, key, value, sep, orig): self.key = key self.value = value self.sep = sep self.orig = orig def __eq__(self, other): return self.__dict__ == other.__dict__ def __repr__(self): return repr(self.__dict__) class SessionNameValidator(object): def __init__(self, error_message): self.error_message = error_message def __call__(self, value): # Session name can be a path or just a name. if (os.path.sep not in value and not VALID_SESSION_NAME_PATTERN.search(value)): raise ArgumentError(None, self.error_message) return value class KeyValueArgType(object): """A key-value pair argument type used with `argparse`. Parses a key-value arg and constructs a `KeyValue` instance. Used for headers, form data, and other key-value pair types. """ key_value_class = KeyValue def __init__(self, *separators): self.separators = separators self.special_characters = set('\\') for separator in separators: self.special_characters.update(separator) def __call__(self, string): """Parse `string` and return `self.key_value_class()` instance. The best of `self.separators` is determined (first found, longest). Back slash escaped characters aren't considered as separators (or parts thereof). Literal back slash characters have to be escaped as well (r'\\'). """ class Escaped(str): """Represents an escaped character.""" def tokenize(string): """Tokenize `string`. There are only two token types - strings and escaped characters: tokenize(r'foo\=bar\\baz') => ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] """ tokens = [''] characters = iter(string) for char in characters: if char == '\\': char = next(characters, '') if char not in self.special_characters: tokens[-1] += '\\' + char else: tokens.extend([Escaped(char), '']) else: tokens[-1] += char return tokens tokens = tokenize(string) # Sorting by length ensures that the longest one will be # chosen as it will overwrite any shorter ones starting # at the same position in the `found` dictionary. separators = sorted(self.separators, key=len) for i, token in enumerate(tokens): if isinstance(token, Escaped): continue found = {} for sep in separators: pos = token.find(sep) if pos != -1: found[pos] = sep if found: # Starting first, longest separator found. sep = found[min(found.keys())] key, value = token.split(sep, 1) # Any preceding tokens are part of the key. key = ''.join(tokens[:i]) + key # Any following tokens are part of the value. value += ''.join(tokens[i + 1:]) break else: raise ArgumentTypeError( u'"%s" is not a valid value' % string) return self.key_value_class( key=key, value=value, sep=sep, orig=string) class AuthCredentials(KeyValue): """Represents parsed credentials.""" def _getpass(self, prompt): # To allow mocking. return getpass.getpass(str(prompt)) def has_password(self): return self.value is not None def prompt_password(self, host): try: self.value = self._getpass( 'http: password for %s@%s: ' % (self.key, host)) except (EOFError, KeyboardInterrupt): sys.stderr.write('\n') sys.exit(0) class AuthCredentialsArgType(KeyValueArgType): """A key-value arg type that parses credentials.""" key_value_class = AuthCredentials def __call__(self, string): """Parse credentials from `string`. ("username" or "username:password"). """ try: return super(AuthCredentialsArgType, self).__call__(string) except ArgumentTypeError: # No password provided, will prompt for it later. return self.key_value_class( key=string, value=None, sep=SEP_CREDENTIALS, orig=string ) parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS) class RequestItemsDict(OrderedDict): """Multi-value dict for URL parameters and form data.""" if is_pypy and is_py27: # Manually set keys when initialized with an iterable as PyPy # doesn't call __setitem__ in such case (pypy3 does). def __init__(self, *args, **kwargs): if len(args) == 1 and isinstance(args[0], Iterable): super(RequestItemsDict, self).__init__(**kwargs) for k, v in args[0]: self[k] = v else: super(RequestItemsDict, self).__init__(*args, **kwargs) # noinspection PyMethodOverriding def __setitem__(self, key, value): """ If `key` is assigned more than once, `self[key]` holds a `list` of all the values. This allows having multiple fields with the same name in form data and URL params. """ assert not isinstance(value, list) if key not in self: super(RequestItemsDict, self).__setitem__(key, value) else: if not isinstance(self[key], list): super(RequestItemsDict, self).__setitem__(key, [self[key]]) self[key].append(value) class ParamsDict(RequestItemsDict): pass class DataDict(RequestItemsDict): def items(self): for key, values in super(RequestItemsDict, self).items(): if not isinstance(values, list): values = [values] for value in values: yield key, value RequestItems = namedtuple('RequestItems', ['headers', 'data', 'files', 'params']) def get_content_type(filename): """ Return the content type for ``filename`` in format appropriate for Content-Type headers, or ``None`` if the file type is unknown to ``mimetypes``. """ mime, encoding = mimetypes.guess_type(filename, strict=False) if mime: content_type = mime if encoding: content_type = '%s; charset=%s' % (mime, encoding) return content_type def parse_items(items, headers_class=CaseInsensitiveDict, data_class=OrderedDict, files_class=DataDict, params_class=ParamsDict): """Parse `KeyValue` `items` into `data`, `headers`, `files`, and `params`. """ headers = [] data = [] files = [] params = [] for item in items: value = item.value if item.sep == SEP_HEADERS: if value == '': # No value => unset the header value = None target = headers elif item.sep == SEP_HEADERS_EMPTY: if item.value: raise ParseError( 'Invalid item "%s" ' '(to specify an empty header use `Header;`)' % item.orig ) target = headers elif item.sep == SEP_QUERY: target = params elif item.sep == SEP_FILES: try: with open(os.path.expanduser(value), 'rb') as f: value = (os.path.basename(value), BytesIO(f.read()), get_content_type(value)) except IOError as e: raise ParseError('"%s": %s' % (item.orig, e)) target = files elif item.sep in SEP_GROUP_DATA_ITEMS: if item.sep in SEP_GROUP_DATA_EMBED_ITEMS: try: with open(os.path.expanduser(value), 'rb') as f: value = f.read().decode('utf8') except IOError as e: raise ParseError('"%s": %s' % (item.orig, e)) except UnicodeDecodeError: raise ParseError( '"%s": cannot embed the content of "%s",' ' not a UTF8 or ASCII-encoded text file' % (item.orig, item.value) ) if item.sep in SEP_GROUP_RAW_JSON_ITEMS: try: value = load_json_preserve_order(value) except ValueError as e: raise ParseError('"%s": %s' % (item.orig, e)) target = data else: raise TypeError(item) target.append((item.key, value)) return RequestItems(headers_class(headers), data_class(data), files_class(files), params_class(params)) def readable_file_arg(filename): try: open(filename, 'rb') except IOError as ex: raise ArgumentTypeError('%s: %s' % (filename, ex.args[1])) return filename httpie-0.9.8/httpie/plugins/0000755000000000000000000000000013022157774014511 5ustar rootroothttpie-0.9.8/httpie/plugins/__init__.py0000644000000000000000000000137013022157774016623 0ustar rootroot""" WARNING: The plugin API is still work in progress and will probably be completely reworked by v1.0.0. """ from httpie.plugins.base import ( AuthPlugin, FormatterPlugin, ConverterPlugin, TransportPlugin ) from httpie.plugins.manager import PluginManager from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin from httpie.output.formatters.headers import HeadersFormatter from httpie.output.formatters.json import JSONFormatter from httpie.output.formatters.colors import ColorFormatter plugin_manager = PluginManager() plugin_manager.register(BasicAuthPlugin, DigestAuthPlugin) plugin_manager.register(HeadersFormatter, JSONFormatter, ColorFormatter) httpie-0.9.8/httpie/plugins/base.py0000644000000000000000000000565613022157774016011 0ustar rootrootclass BasePlugin(object): # The name of the plugin, eg. "My auth". name = None # Optional short description. Will be be shown in the help # under --auth-type. description = None # This be set automatically once the plugin has been loaded. package_name = None class AuthPlugin(BasePlugin): """ Base auth plugin class. See for an example auth plugin. See also `test_auth_plugins.py` """ # The value that should be passed to --auth-type # to use this auth plugin. Eg. "my-auth" auth_type = None # Set to `False` to make it possible to invoke this auth # plugin without requiring the user to specify credentials # through `--auth, -a`. auth_require = True # By default the `-a` argument is parsed for `username:password`. # Set this to `False` to disable the parsing and error handling. auth_parse = True # If both `auth_parse` and `prompt_password` are set to `True`, # and the value of `-a` lacks the password part, # then the user will be prompted to type the password in. prompt_password = True # Will be set to the raw value of `-a` (if provided) before # `get_auth()` gets called. raw_auth = None def get_auth(self, username=None, password=None): """ If `auth_parse` is set to `True`, then `username` and `password` contain the parsed credentials. Use `self.raw_auth` to access the raw value passed through `--auth, -a`. Return a ``requests.auth.AuthBase`` subclass instance. """ raise NotImplementedError() class TransportPlugin(BasePlugin): """ http://docs.python-requests.org/en/latest/user/advanced/#transport-adapters """ # The URL prefix the adapter should be mount to. prefix = None def get_adapter(self): """ Return a ``requests.adapters.BaseAdapter`` subclass instance to be mounted to ``self.prefix``. """ raise NotImplementedError() class ConverterPlugin(object): def __init__(self, mime): self.mime = mime def convert(self, content_bytes): raise NotImplementedError @classmethod def supports(cls, mime): raise NotImplementedError class FormatterPlugin(object): def __init__(self, **kwargs): """ :param env: an class:`Environment` instance :param kwargs: additional keyword argument that some processor might require. """ self.enabled = True self.kwargs = kwargs def format_headers(self, headers): """Return processed `headers` :param headers: The headers as text. """ return headers def format_body(self, content, mime): """Return processed `content`. :param mime: E.g., 'application/atom+xml'. :param content: The body content as text """ return content httpie-0.9.8/httpie/plugins/builtin.py0000644000000000000000000000236413022157774016536 0ustar rootrootfrom base64 import b64encode import requests.auth from httpie.plugins.base import AuthPlugin # noinspection PyAbstractClass class BuiltinAuthPlugin(AuthPlugin): package_name = '(builtin)' class HTTPBasicAuth(requests.auth.HTTPBasicAuth): def __call__(self, r): """ Override username/password serialization to allow unicode. See https://github.com/jkbrzt/httpie/issues/212 """ r.headers['Authorization'] = type(self).make_header( self.username, self.password).encode('latin1') return r @staticmethod def make_header(username, password): credentials = u'%s:%s' % (username, password) token = b64encode(credentials.encode('utf8')).strip().decode('latin1') return 'Basic %s' % token class BasicAuthPlugin(BuiltinAuthPlugin): name = 'Basic HTTP auth' auth_type = 'basic' # noinspection PyMethodOverriding def get_auth(self, username, password): return HTTPBasicAuth(username, password) class DigestAuthPlugin(BuiltinAuthPlugin): name = 'Digest HTTP auth' auth_type = 'digest' # noinspection PyMethodOverriding def get_auth(self, username, password): return requests.auth.HTTPDigestAuth(username, password) httpie-0.9.8/httpie/plugins/manager.py0000644000000000000000000000401613022157774016476 0ustar rootrootfrom itertools import groupby from pkg_resources import iter_entry_points from httpie.plugins import AuthPlugin, FormatterPlugin, ConverterPlugin from httpie.plugins.base import TransportPlugin ENTRY_POINT_NAMES = [ 'httpie.plugins.auth.v1', 'httpie.plugins.formatter.v1', 'httpie.plugins.converter.v1', 'httpie.plugins.transport.v1', ] class PluginManager(object): def __init__(self): self._plugins = [] def __iter__(self): return iter(self._plugins) def register(self, *plugins): for plugin in plugins: self._plugins.append(plugin) def unregister(self, plugin): self._plugins.remove(plugin) def load_installed_plugins(self): for entry_point_name in ENTRY_POINT_NAMES: for entry_point in iter_entry_points(entry_point_name): plugin = entry_point.load() plugin.package_name = entry_point.dist.key self.register(entry_point.load()) # Auth def get_auth_plugins(self): return [plugin for plugin in self if issubclass(plugin, AuthPlugin)] def get_auth_plugin_mapping(self): return dict((plugin.auth_type, plugin) for plugin in self.get_auth_plugins()) def get_auth_plugin(self, auth_type): return self.get_auth_plugin_mapping()[auth_type] # Output processing def get_formatters(self): return [plugin for plugin in self if issubclass(plugin, FormatterPlugin)] def get_formatters_grouped(self): groups = {} for group_name, group in groupby( self.get_formatters(), key=lambda p: getattr(p, 'group_name', 'format')): groups[group_name] = list(group) return groups def get_converters(self): return [plugin for plugin in self if issubclass(plugin, ConverterPlugin)] # Adapters def get_transport_plugins(self): return [plugin for plugin in self if issubclass(plugin, TransportPlugin)] httpie-0.9.8/httpie/client.py0000644000000000000000000001224713022157774014666 0ustar rootrootimport json import sys import requests from requests.adapters import HTTPAdapter from requests.packages import urllib3 from httpie import sessions from httpie import __version__ from httpie.compat import str from httpie.input import SSL_VERSION_ARG_MAPPING from httpie.plugins import plugin_manager from httpie.utils import repr_dict_nice try: # https://urllib3.readthedocs.io/en/latest/security.html urllib3.disable_warnings() except AttributeError: # In some rare cases, the user may have an old version of the requests # or urllib3, and there is no method called "disable_warnings." In these # cases, we don't need to call the method. # They may get some noisy output but execution shouldn't die. Move on. pass FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' JSON_CONTENT_TYPE = 'application/json' JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE) DEFAULT_UA = 'HTTPie/%s' % __version__ class HTTPieHTTPAdapter(HTTPAdapter): def __init__(self, ssl_version=None, **kwargs): self._ssl_version = ssl_version super(HTTPieHTTPAdapter, self).__init__(**kwargs) def init_poolmanager(self, *args, **kwargs): kwargs['ssl_version'] = self._ssl_version super(HTTPieHTTPAdapter, self).init_poolmanager(*args, **kwargs) def get_requests_session(ssl_version): requests_session = requests.Session() requests_session.mount( 'https://', HTTPieHTTPAdapter(ssl_version=ssl_version) ) for cls in plugin_manager.get_transport_plugins(): transport_plugin = cls() requests_session.mount(prefix=transport_plugin.prefix, adapter=transport_plugin.get_adapter()) return requests_session def get_response(args, config_dir): """Send the request and return a `request.Response`.""" ssl_version = None if args.ssl_version: ssl_version = SSL_VERSION_ARG_MAPPING[args.ssl_version] requests_session = get_requests_session(ssl_version) requests_session.max_redirects = args.max_redirects if not args.session and not args.session_read_only: kwargs = get_requests_kwargs(args) if args.debug: dump_request(kwargs) response = requests_session.request(**kwargs) else: response = sessions.get_response( requests_session=requests_session, args=args, config_dir=config_dir, session_name=args.session or args.session_read_only, read_only=bool(args.session_read_only), ) return response def dump_request(kwargs): sys.stderr.write('\n>>> requests.request(**%s)\n\n' % repr_dict_nice(kwargs)) def finalize_headers(headers): final_headers = {} for name, value in headers.items(): if value is not None: # >leading or trailing LWS MAY be removed without # >changing the semantics of the field value" # -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html # Also, requests raises `InvalidHeader` for leading spaces. value = value.strip() if isinstance(value, str): # See: https://github.com/jkbrzt/httpie/issues/212 value = value.encode('utf8') final_headers[name] = value return final_headers def get_default_headers(args): default_headers = { 'User-Agent': DEFAULT_UA } auto_json = args.data and not args.form if args.json or auto_json: default_headers['Accept'] = JSON_ACCEPT if args.json or (auto_json and args.data): default_headers['Content-Type'] = JSON_CONTENT_TYPE elif args.form and not args.files: # If sending files, `requests` will set # the `Content-Type` for us. default_headers['Content-Type'] = FORM_CONTENT_TYPE return default_headers def get_requests_kwargs(args, base_headers=None): """ Translate our `args` into `requests.request` keyword arguments. """ # Serialize JSON data, if needed. data = args.data auto_json = data and not args.form if (args.json or auto_json) and isinstance(data, dict): if data: data = json.dumps(data) else: # We need to set data to an empty string to prevent requests # from assigning an empty list to `response.request.data`. data = '' # Finalize headers. headers = get_default_headers(args) if base_headers: headers.update(base_headers) headers.update(args.headers) headers = finalize_headers(headers) cert = None if args.cert: cert = args.cert if args.cert_key: cert = cert, args.cert_key kwargs = { 'stream': True, 'method': args.method.lower(), 'url': args.url, 'headers': headers, 'data': data, 'verify': { 'yes': True, 'no': False }.get(args.verify, args.verify), 'cert': cert, 'timeout': args.timeout, 'auth': args.auth, 'proxies': dict((p.key, p.value) for p in args.proxy), 'files': args.files, 'allow_redirects': args.follow, 'params': args.params, } return kwargs httpie-0.9.8/appveyor.yml0000644000000000000000000000105313022157774014122 0ustar rootroot# https://ci.appveyor.com/project/jkbrzt/httpie build: false environment: matrix: - PYTHON: "C:/Python27" # Python 3.4 has outdated pip # - PYTHON: "C:/Python34" - PYTHON: "C:/Python35" init: - "ECHO %PYTHON%" - ps: "ls C:/Python*" install: # FIXME: updating pip fails with PermissionError # - "%PYTHON%/Scripts/pip.exe install -U pip setuptools" - "%PYTHON%/Scripts/pip.exe install -e ." test_script: - "%PYTHON%/Scripts/pip.exe --version" - "%PYTHON%/Scripts/http.exe --debug" - "%PYTHON%/python.exe setup.py test" httpie-0.9.8/setup.py0000644000000000000000000000615013022157774013247 0ustar rootroot# This is purely the result of trial and error. import sys import codecs from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand import httpie class PyTest(TestCommand): # `$ python setup.py test' simply installs minimal requirements # and runs the tests with no fancy stuff like parallel execution. def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [ '--doctest-modules', '--verbose', './httpie', './tests' ] self.test_suite = True def run_tests(self): import pytest sys.exit(pytest.main(self.test_args)) tests_require = [ # Pytest needs to come last. # https://bitbucket.org/pypa/setuptools/issue/196/ 'pytest-httpbin', 'pytest', 'mock', ] install_requires = [ 'requests>=2.11.0', 'Pygments>=2.1.3' ] # Conditional dependencies: # sdist if 'bdist_wheel' not in sys.argv: try: # noinspection PyUnresolvedReferences import argparse except ImportError: install_requires.append('argparse>=1.2.1') if 'win32' in str(sys.platform).lower(): # Terminal colors for Windows install_requires.append('colorama>=0.2.4') # bdist_wheel extras_require = { # http://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies ':python_version == "2.6"' ' or python_version == "3.0"' ' or python_version == "3.1" ': ['argparse>=1.2.1'], ':sys_platform == "win32"': ['colorama>=0.2.4'], } def long_description(): with codecs.open('README.rst', encoding='utf8') as f: return f.read() setup( name='httpie', version=httpie.__version__, description=httpie.__doc__.strip(), long_description=long_description(), url='http://httpie.org/', download_url='https://github.com/jkbrzt/httpie', author=httpie.__author__, author_email='jakub@roztocil.co', license=httpie.__licence__, packages=find_packages(), entry_points={ 'console_scripts': [ 'http = httpie.__main__:main', ], }, extras_require=extras_require, install_requires=install_requires, tests_require=tests_require, cmdclass={'test': PyTest}, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: BSD License', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development', 'Topic :: System :: Networking', 'Topic :: Terminals', 'Topic :: Text Processing', 'Topic :: Utilities' ], ) httpie-0.9.8/LICENSE0000644000000000000000000000274013022157774012543 0ustar rootrootCopyright © 2012-2016 Jakub Roztocil Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 3. Neither the name of The author 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 AUTHOR 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 AUTHOR AND 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. httpie-0.9.8/.travis.yml0000644000000000000000000000361113022157774013645 0ustar rootroot# https://travis-ci.org/jkbrzt/httpie sudo: false language: python os: - linux env: global: - NEWEST_PYTHON=3.5 python: - 2.6 - 2.7 - pypy - 3.4 - 3.5 # Currently fails because of a Flask issue # - pypy3 matrix: include: # Manually defined OS X builds # https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages) # Stock OSX Python - os: osx language: generic env: - TOXENV=py27 # Latest Python 2.x from Homebrew - os: osx language: generic env: - TOXENV=py27 - BREW_INSTALL=python # Latest Python 3.x from Homebrew - os: osx language: generic env: - TOXENV=py35 - BREW_INSTALL=python3 # Python Codestyle - os: linux python: 3.5 env: CODESTYLE=true install: - | if [[ $TRAVIS_OS_NAME == 'osx' ]]; then if [[ -n "$BREW_INSTALL" ]]; then brew update brew install "$BREW_INSTALL" fi sudo pip install tox fi if [[ $CODESTYLE ]]; then pip install pycodestyle fi script: - | if [[ $TRAVIS_OS_NAME == 'linux' ]]; then if [[ $CODESTYLE ]]; then # 241 - multiple spaces after ‘,’ # 501 - line too long pycodestyle --ignore=E241,E501 else make fi else PATH="/usr/local/bin:$PATH" tox -e "$TOXENV" fi after_success: - | if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then pip install python-coveralls && coveralls fi notifications: webhooks: urls: # https://gitter.im/jkbrzt/httpie - https://webhooks.gitter.im/e/c42fcd359a110d02830b on_success: always # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: always # options: [always|never|change] default: always httpie-0.9.8/MANIFEST.in0000644000000000000000000000011513022157774013266 0ustar rootrootinclude LICENSE include README.rst include CHANGELOG.rst include AUTHORS.rst httpie-0.9.8/CHANGELOG.rst0000644000000000000000000002562313022157774013564 0ustar rootroot========== Change Log ========== This document records all notable changes to `HTTPie `_. This project adheres to `Semantic Versioning `_. `1.0.0-dev`_ (unreleased) ------------------------- `0.9.8`_ (2016-12-08) --------------------- * Extended auth plugin API. * Added exit status code ``7`` for plugin errors. * Added support for ``curses``-less Python installations. * Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required. * Improved ``CTRL-C`` interrupt handling. * Added the standard exit status code ``130`` for keyboard interrupts. `0.9.6`_ (2016-08-13) --------------------- * Added Python 3 as a dependency for Homebrew installations to ensure some of the newer HTTP features work out of the box for macOS users (starting with HTTPie 0.9.4.). * Added the ability to unset a request header with ``Header:``, and send an empty value with ``Header;``. * Added ``--default-scheme `` to enable things like ``$ alias https='http --default-scheme=https``. * Added ``-I`` as a shortcut for ``--ignore-stdin``. * Added fish shell completion (located in ``extras/httpie-completion.fish`` in the Github repo). * Updated ``requests`` to 2.10.0 so that SOCKS support can be added via ``pip install requests[socks]``. * Changed the default JSON ``Accept`` header from ``application/json`` to ``application/json, */*``. * Changed the pre-processing of request HTTP headers so that any leading and trailing whitespace is removed. `0.9.4`_ (2016-07-01) --------------------- * Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests * Added ``--ssl=`` to specify the desired SSL/TLS protocol version to use for HTTPS requests. * Added JSON detection with ``--json, -j`` to work around incorrect ``Content-Type`` * Added ``--all`` to show intermediate responses such as redirects (with ``--follow``) * Added ``--history-print, -P WHAT`` to specify formatting of intermediate responses * Added ``--max-redirects=N`` (default 30) * Added ``-A`` as short name for ``--auth-type`` * Added ``-F`` as short name for ``--follow`` * Removed the ``implicit_content_type`` config option (use ``"default_options": ["--form"]`` instead) * Redirected ``stdout`` doesn't trigger an error anymore when ``--output FILE`` is set * Changed the default ``--style`` back to ``solarized`` for better support of light and dark terminals * Improved ``--debug`` output * Fixed ``--session`` when used with ``--download`` * Fixed ``--download`` to trim too long filenames before saving the file * Fixed the handling of ``Content-Type`` with multiple ``+subtype`` parts * Removed the XML formatter as the implementation suffered from multiple issues `0.9.3`_ (2016-01-01) --------------------- * Changed the default color ``--style`` from ``solarized`` to ``monokai`` * Added basic Bash autocomplete support (need to be installed manually) * Added request details to connection error messages * Fixed ``'requests.packages.urllib3' has no attribute 'disable_warnings'`` errors that occurred in some installations * Fixed colors and formatting on Windows * Fixed ``--auth`` prompt on Windows `0.9.2`_ (2015-02-24) --------------------- * Fixed compatibility with Requests 2.5.1 * Changed the default JSON ``Content-Type`` to ``application/json`` as UTF-8 is the default JSON encoding `0.9.1`_ (2015-02-07) --------------------- * Added support for Requests transport adapter plugins (see `httpie-unixsocket `_ and `httpie-http2 `_) `0.9.0`_ (2015-01-31) --------------------- * Added ``--cert`` and ``--cert-key`` parameters to specify a client side certificate and private key for SSL * Improved unicode support * Improved terminal color depth detection via ``curses`` * To make it easier to deal with Windows paths in request items, ``\`` now only escapes special characters (the ones that are used as key-value separators by HTTPie) * Switched from ``unittest`` to ``pytest`` * Added Python `wheel` support * Various test suite improvements * Added ``CONTRIBUTING`` * Fixed ``User-Agent`` overwriting when used within a session * Fixed handling of empty passwords in URL credentials * Fixed multiple file uploads with the same form field name * Fixed ``--output=/dev/null`` on Linux * Miscellaneous bugfixes `0.8.0`_ (2014-01-25) --------------------- * Added ``field=@file.txt`` and ``field:=@file.json`` for embedding the contents of text and JSON files into request data * Added curl-style shorthand for localhost * Fixed request ``Host`` header value output so that it doesn't contain credentials, if included in the URL `0.7.1`_ (2013-09-24) --------------------- * Added ``--ignore-stdin`` * Added support for auth plugins * Improved ``--help`` output * Improved ``Content-Disposition`` parsing for ``--download`` mode * Update to Requests 2.0.0 `0.6.0`_ (2013-06-03) --------------------- * XML data is now formatted * ``--session`` and ``--session-read-only`` now also accept paths to session files (eg. ``http --session=/tmp/session.json example.org``) `0.5.1`_ (2013-05-13) --------------------- * ``Content-*`` and ``If-*`` request headers are not stored in sessions anymore as they are request-specific `0.5.0`_ (2013-04-27) --------------------- * Added a download mode via ``--download`` * Fixes miscellaneous bugs `0.4.1`_ (2013-02-26) --------------------- * Fixed ``setup.py`` `0.4.0`_ (2013-02-22) --------------------- * Added Python 3.3 compatibility * Added Requests >= v1.0.4 compatibility * Added support for credentials in URL * Added ``--no-option`` for every ``--option`` to be config-friendly * Mutually exclusive arguments can be specified multiple times. The last value is used `0.3.0`_ (2012-09-21) --------------------- * Allow output redirection on Windows * Added configuration file * Added persistent session support * Renamed ``--allow-redirects`` to ``--follow`` * Improved the usability of ``http --help`` * Fixed installation on Windows with Python 3 * Fixed colorized output on Windows with Python 3 * CRLF HTTP header field separation in the output * Added exit status code ``2`` for timed-out requests * Added the option to separate colorizing and formatting (``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``) ``--ugly`` has bee removed in favor of ``--pretty=none`` `0.2.7`_ (2012-08-07) --------------------- * Added compatibility with Requests 0.13.6 * Added streamed terminal output. ``--stream, -S`` can be used to enable streaming also with ``--pretty`` and to ensure a more frequent output flushing * Added support for efficient large file downloads * Sort headers by name (unless ``--pretty=none``) * Response body is fetched only when needed (e.g., not with ``--headers``) * Improved content type matching * Updated Solarized color scheme * Windows: Added ``--output FILE`` to store output into a file (piping results in corrupted data on Windows) * Proper handling of binary requests and responses * Fixed printing of ``multipart/form-data`` requests * Renamed ``--traceback`` to ``--debug`` `0.2.6`_ (2012-07-26) --------------------- * The short option for ``--headers`` is now ``-h`` (``-t`` has been removed, for usage use ``--help``) * Form data and URL parameters can have multiple fields with the same name (e.g.,``http -f url a=1 a=2``) * Added ``--check-status`` to exit with an error on HTTP 3xx, 4xx and 5xx (3, 4, and 5, respectively) * If the output is piped to another program or redirected to a file, the default behaviour is to only print the response body (It can still be overwritten via the ``--print`` flag.) * Improved highlighting of HTTP headers * Added query string parameters (``param==value``) * Added support for terminal colors under Windows `0.2.5`_ (2012-07-17) --------------------- * Unicode characters in prettified JSON now don't get escaped for improved readability * --auth now prompts for a password if only a username provided * Added support for request payloads from a file path with automatic ``Content-Type`` (``http URL @/path``) * Fixed missing query string when displaying the request headers via ``--verbose`` * Fixed Content-Type for requests with no data `0.2.2`_ (2012-06-24) --------------------- * The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data) * Fixed --verbose --form * Added support for Tox `0.2.1`_ (2012-06-13) --------------------- * Added compatibility with ``requests-0.12.1`` * Dropped custom JSON and HTTP lexers in favor of the ones newly included in ``pygments-1.5`` `0.2.0`_ (2012-04-25) --------------------- * Added Python 3 support * Added the ability to print the HTTP request as well as the response (see ``--print`` and ``--verbose``) * Added support for Digest authentication * Added file upload support (``http -f POST file_field_name@/path/to/file``) * Improved syntax highlighting for JSON * Added support for field name escaping * Many bug fixes `0.1.6`_ (2012-03-04) --------------------- * Fixed ``setup.py`` `0.1.5`_ (2012-03-04) --------------------- * Many improvements and bug fixes `0.1.4`_ (2012-02-28) --------------------- * Many improvements and bug fixes `0.1`_ (2012-02-25) ------------------- * Initial public release .. _`0.1`: https://github.com/jkbrzt/httpie/commit/b966efa .. _0.1.4: https://github.com/jkbrzt/httpie/compare/b966efa...0.1.4 .. _0.1.5: https://github.com/jkbrzt/httpie/compare/0.1.4...0.1.5 .. _0.1.6: https://github.com/jkbrzt/httpie/compare/0.1.5...0.1.6 .. _0.2.0: https://github.com/jkbrzt/httpie/compare/0.1.6...0.2.0 .. _0.2.1: https://github.com/jkbrzt/httpie/compare/0.2.0...0.2.1 .. _0.2.2: https://github.com/jkbrzt/httpie/compare/0.2.1...0.2.2 .. _0.2.5: https://github.com/jkbrzt/httpie/compare/0.2.2...0.2.5 .. _0.2.6: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.6 .. _0.2.7: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.7 .. _0.3.0: https://github.com/jkbrzt/httpie/compare/0.2.7...0.3.0 .. _0.4.0: https://github.com/jkbrzt/httpie/compare/0.3.0...0.4.0 .. _0.4.1: https://github.com/jkbrzt/httpie/compare/0.4.0...0.4.1 .. _0.5.0: https://github.com/jkbrzt/httpie/compare/0.4.1...0.5.0 .. _0.5.1: https://github.com/jkbrzt/httpie/compare/0.5.0...0.5.1 .. _0.6.0: https://github.com/jkbrzt/httpie/compare/0.5.1...0.6.0 .. _0.7.1: https://github.com/jkbrzt/httpie/compare/0.6.0...0.7.1 .. _0.8.0: https://github.com/jkbrzt/httpie/compare/0.7.1...0.8.0 .. _0.9.0: https://github.com/jkbrzt/httpie/compare/0.8.0...0.9.0 .. _0.9.1: https://github.com/jkbrzt/httpie/compare/0.9.0...0.9.1 .. _0.9.2: https://github.com/jkbrzt/httpie/compare/0.9.1...0.9.2 .. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3 .. _0.9.4: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.4 .. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.4...0.9.6 .. _0.9.8: https://github.com/jkbrzt/httpie/compare/0.9.6...0.9.8 .. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.8...master httpie-0.9.8/extras/0000755000000000000000000000000013022157774013041 5ustar rootroothttpie-0.9.8/extras/get-homebrew-formula-vars.py0000755000000000000000000000240013022157774020413 0ustar rootroot#!/usr/bin/env python """ Generate URLs and file hashes to be included in the Homebrew formula after a new release of HTTPie is published on PyPi. https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb """ import hashlib import requests PACKAGES = [ 'httpie', 'requests', 'pygments', ] def get_info(package_name): api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name) resp = requests.get(api_url).json() hasher = hashlib.sha256() for release in resp['urls']: download_url = release['url'] if download_url.endswith('.tar.gz'): hasher.update(requests.get(download_url).content) return { 'name': package_name, 'url': download_url, 'sha256': hasher.hexdigest(), } else: raise RuntimeError( '{}: download not found: {}'.format(package_name, resp)) packages = { package_name: get_info(package_name) for package_name in PACKAGES } httpie_info = packages.pop('httpie') print(""" url "{url}" sha256 "{sha256}" """.format(**httpie_info)) for package_info in packages.values(): print(""" resource "{name}" do url "{url}" sha256 "{sha256}" end""".format(**package_info)) httpie-0.9.8/extras/httpie-completion.bash0000644000000000000000000000133313022157774017344 0ustar rootroot#!/usr/bin/env bash _http_complete() { local cur_word=${COMP_WORDS[COMP_CWORD]} local prev_word=${COMP_WORDS[COMP_CWORD - 1]} if [[ "$cur_word" == -* ]]; then _http_complete_options "$cur_word" fi } complete -o default -F _http_complete http _http_complete_options() { local cur_word=$1 local options="-j --json -f --form --pretty -s --style -p --print -v --verbose -h --headers -b --body -S --stream -o --output -d --download -c --continue --session --session-read-only -a --auth --auth-type --proxy --follow --verify --cert --cert-key --timeout --check-status --ignore-stdin --help --version --traceback --debug" COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) ) } httpie-0.9.8/extras/httpie-completion.fish0000644000000000000000000000626213022157774017366 0ustar rootrootfunction __fish_httpie_auth_types echo "basic"\t"Basic HTTP auth" echo "digest"\t"Digest HTTP auth" end function __fish_httpie_styles echo "autumn" echo "borland" echo "bw" echo "colorful" echo "default" echo "emacs" echo "friendly" echo "fruity" echo "igor" echo "manni" echo "monokai" echo "murphy" echo "native" echo "paraiso-dark" echo "paraiso-light" echo "pastie" echo "perldoc" echo "rrt" echo "solarized" echo "tango" echo "trac" echo "vim" echo "vs" echo "xcode" end complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode" complete -c http -s f -l form -d 'Data items from the command line are serialized as form fields' complete -c http -s j -l json -d '(default) Data items from the command line are serialized as a JSON object' complete -x -c http -l pretty -d 'Controls output processing' -a "all colors format none" -A complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode" complete -x -c http -s p -l print -d 'String specifying what the output should contain' complete -c http -s v -l verbose -d 'Print the whole request as well as the response' complete -c http -s h -l headers -d 'Print only the response headers' complete -c http -s b -l body -d 'Print only the response body' complete -c http -s S -l stream -d 'Always stream the output by line' complete -c http -s o -l output -d 'Save output to FILE' complete -c http -s d -l download -d 'Do not print the response body to stdout' complete -c http -s c -l continue -d 'Resume an interrupted download' complete -x -c http -l session -d 'Create, or reuse and update a session' complete -x -c http -s a -l auth -d 'If only the username is provided (-a username), HTTPie will prompt for the password' complete -x -c http -l auth-type -d 'The authentication mechanism to be used' -a '(__fish_httpie_auth_types)' -A complete -x -c http -l proxy -d 'String mapping protocol to the URL of the proxy' complete -c http -l follow -d 'Allow full redirects' complete -x -c http -l verify -d 'SSL cert verification' complete -c http -l cert -d 'SSL cert' complete -c http -l cert-key -d 'Private SSL cert key' complete -x -c http -l timeout -d 'Connection timeout in seconds' complete -c http -l check-status -d 'Error with non-200 HTTP status code' complete -c http -l ignore-stdin -d 'Do not attempt to read stdin' complete -c http -l help -d 'Show help' complete -c http -l version -d 'Show version' complete -c http -l traceback -d 'Prints exception traceback should one occur' complete -c http -l debug -d 'Show debugging information' httpie-0.9.8/extras/httpie.rb0000644000000000000000000000357613022157774014676 0ustar rootroot# The latest Homebrew formula as submitted to Homebrew/homebrew-core. # Only useful for testing until it gets accepted by homebrew maintainers. # # https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb # class Httpie < Formula desc "User-friendly cURL replacement (command-line HTTP client)" homepage "https://httpie.org/" url "https://pypi.python.org/packages/10/cf/da63860ef92f9c90a5bd684d5f162067b26ef113b1c4afb9979c2f5c5a7a/httpie-0.9.7.tar.gz" sha256 "6427c198c80b04e84963890261f29f1e3452b2b4b81e87a403bf22996754e6ec" head "https://github.com/jkbrzt/httpie.git" depends_on :python3 resource "requests" do url "https://pypi.python.org/packages/d9/03/155b3e67fe35fe5b6f4227a8d9e96a14fda828b18199800d161bcefc1359/requests-2.12.3.tar.gz" sha256 "de5d266953875e9647e37ef7bfe6ef1a46ff8ddfe61b5b3652edf7ea717ee2b2" end resource "pygments" do url "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz" sha256 "88e4c8a91b2af5962bfa5ea2447ec6dd357018e86e94c7d14bd8cacbc5b55d81" end def install pyver = Language::Python.major_minor_version "python3" ENV.prepend_create_path "PYTHONPATH", libexec/"vendor/lib/python#{pyver}/site-packages" %w[pygments requests].each do |r| resource(r).stage do system "python3", *Language::Python.setup_install_args(libexec/"vendor") end end ENV.prepend_create_path "PYTHONPATH", libexec/"lib/python#{pyver}/site-packages" system "python3", *Language::Python.setup_install_args(libexec) bin.install Dir["#{libexec}/bin/*"] bin.env_script_all_files(libexec/"bin", :PYTHONPATH => ENV["PYTHONPATH"]) end test do raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb" assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}") end end httpie-0.9.8/.coveragerc0000644000000000000000000000005713022157774013656 0ustar rootroot; needs to exist otherwise `$ coveralls` fails httpie-0.9.8/AUTHORS.rst0000644000000000000000000000277713022157774013427 0ustar rootroot============== HTTPie authors ============== * `Jakub Roztocil `_ Patches and ideas ----------------- `Complete list of contributors on GitHub `_ * `Cláudia T. Delgado `_ (logo) * `Hank Gay `_ * `Jake Basile `_ * `Vladimir Berkutov `_ * `Jakob Kramer `_ * `Chris Faulkner `_ * `Alen Mujezinovic `_ * `Praful Mathur `_ * `Marc Abramowitz `_ * `Ismail Badawi `_ * `Laurent Bachelier `_ * `Isman Firmansyah `_ * `Simon Olofsson `_ * `Churkin Oleg `_ * `Jökull Sólberg Auðunsson `_ * `Matthew M. Boedicker `_ * `marblar `_ * `Tomek Wójcik `_ * `Davey Shafik `_ * `cido `_ * `Justin Bonnar `_ * `Nathan LaFreniere `_ * `Matthias Lehmann `_ * `Dennis Brakhane `_ * `Matt Layman `_ * `Edward Yang `_ httpie-0.9.8/tox.ini0000644000000000000000000000121013022157774013040 0ustar rootroot# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. See ./CONTRIBUTING.rst [tox] envlist = py26, py27, py35, pypy, codestyle [testenv] deps = mock pytest pytest-httpbin>=0.0.6 commands = # NOTE: the order of the directories in posargs seems to matter. # When changed, then many ImportMismatchError exceptions occurrs. py.test \ --verbose \ --doctest-modules \ {posargs:./httpie ./tests} [testenv:codestyle] deps = pycodestyle commands = pycodestyle \ --ignore=E241,E501 # 241 - multiple spaces after ‘,’ # 501 - line too long httpie-0.9.8/CONTRIBUTING.rst0000644000000000000000000000631613022157774014202 0ustar rootroot###################### Contributing to HTTPie ###################### Bug reports and code and documentation patches are welcome. You can help this project also by using the development version of HTTPie and by reporting any bugs you might encounter. 1. Reporting bugs ================= **It's important that you provide the full command argument list as well as the output of the failing command.** Use the ``--debug`` flag and copy&paste both the command and its output to your bug report, e.g.: .. code-block:: bash $ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR] [COMPLETE OUTPUT] 2. Contributing Code and Docs ============================= Before working on a new feature or a bug, please browse `existing issues`_ to see whether it has been previously discussed. If the change in question is a bigger one, it's always good to discuss before your starting working on it. Creating Development Environment -------------------------------- Go to https://github.com/jkbrzt/httpie and fork the project repository. .. code-block:: bash git clone https://github.com//httpie cd httpie git checkout -b my_topical_branch # (Recommended: create a new virtualenv) # Install dev. requirements and also HTTPie (in editable mode # so that the `http' command will point to your working copy): make Making Changes -------------- Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8). Testing ------- Before opening a pull requests, please make sure the `test suite`_ passes in all of the `supported Python environments`_. You should also add tests for any new features and bug fixes. HTTPie uses `pytest`_ and `Tox`_ for testing. Running all tests: ****************** .. code-block:: bash # Run all tests on the current Python interpreter make test # Run all tests on the current Python with coverage make test-cover # Run all tests in all of the supported and available Pythons via Tox make test-tox # Run all tests for code as well as packaging, etc. make test-all Running specific tests: *********************** .. code-block:: bash # Run specific tests on the current Python py.test tests/test_uploads.py py.test tests/test_uploads.py::TestMultipartFormDataFileUpload py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok # Run specific tests on the on all Pythons via Tox tox -- tests/test_uploads.py --verbose tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose ----- See `Makefile`_ for additional development utilities. Don't forget to add yourself to `AUTHORS`_! .. _Tox: http://tox.testrun.org .. _supported Python environments: https://github.com/jkbrzt/httpie/blob/master/tox.ini .. _existing issues: https://github.com/jkbrzt/httpie/issues?state=open .. _AUTHORS: https://github.com/jkbrzt/httpie/blob/master/AUTHORS.rst .. _Makefile: https://github.com/jkbrzt/httpie/blob/master/Makefile .. _pytest: http://pytest.org/ .. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/ .. _test suite: https://github.com/jkbrzt/httpie/tree/master/tests httpie-0.9.8/httpie.png0000644000000000000000000055417113022157774013553 0ustar rootrootPNG  IHDR 8sRGB@IDATx |\ŕ/6˒Y-˻,/&;/&2$ofLfLf&/ YwBmʼn@":a}aS_qq(jF@#pa"d_qaݪ Drz˖{AuF@#r&{Ko 4DpmCKo7קF@#z鋷xZF8VN4=|ʴzF@#e&^Mx9vX:{'ecil"di4DC ^^OzRVlѥc@"NRO답|V'*:jF@#_Ū#Vy3B=-o֧Ckޖk&]F@#MզޖW.Nkp[G,ʺ33yF@#H ܹ-VшEVET:H8z~7%'ōXN^Rh4#vs#/FU9M$,DuG2N2N<9 nd#+eth4X;7N2N9enKkFHAt*j%򣥙o<\DNlE@. 7FG|IK6I*m <3sٵG0h xI )Zp4 7#P>ޠG׸#oH5iɤK krTU,y< +>rQ 5e)NTfLtO ]5׏;u/Rcۙj\:ת 5|pT鴋fJ39AsVXNk7#8NƹUZheq&` =}k6tz%eڱԐ *0d*fҌ'14ysh%QR"t &gu87ggm[84>EIm ٗaط.HDaj}8x 2O'7Dˑ덏uZF MǪu|wg9f ,#e91As]SKy5oep:* 9PmrLCenOI $z:[܎rLҖ˛-"g5yp6r>DCtn'D}{x׻9LzE۶Iviѭ4S#@E Ёs95aZF Qqcw}iz1/̛2 ^Y-[޲qW4K5Xx,QsqVy;u$<ќSzTVi9V.ō1{oŗ(JmS?5~󏾎S;GlrH/5?-Èq#0)x1[ػM,W(/Ϳ}}Hk37?/yq[Frݜjqy堦CG(DsNrzEK )Vytt4Z LΩI>Z6E_Xl?+>rS >ұ#?C>WscKǃ"=7'z1In䥢YLCJ[uxRPQKMAU,FΩi|!D "&Ve8%~l4M'/? qMG@NJoڳlT}o ~ɰu)Cr`9c{r{FGVl4}RL/GQZ/H6o:\ҠR|? ?~h/w-ސތ478v `::h4]%&6(A@C9NbዬJi*]B?gz%N֖@tL[J-# l4ZgO#rp)? VyƖR,-\[vR#(k(-XV+ڶEnOyƔSaxajod鬹E? dO LBV%(ZGnFϘI~/⾛/y 5X`l\`,<<ڢJA6Ƶ L+ 5U2V<$c@B# H;șj*-4XӜ_*ܲez1)44 yZ$m {ʁט8)8cGoZ|r%Ӽ(kLe Θ9Ժz4o; }cVtjAJlbipG*|4ۙbrR?i)5%E Rj%~uhbG`3T-(^Î h*_N3LS۠F !lx*MK[LS钷:@F@#$vΩ8JM*#t'y569A-twuJFi*qUSZWRƉ<֫F@#8gMcEuJE*͜9k8OIcI.G\nv6iF@#h'cWw>a!JU Mb'X#p^̣\(j45oNs^z8-|XuPt^#h4_%45Ji4y;ױF !e4X] \f̱gK^.vӱF@#h dKOf+!}@Zqp&.bSrIϝ{%0V8Hiji̼Xz3E^ԱF" Eae9Ey"6}'YHNvN[Nfz)m'@A@zOŢ+wf Ɵ>U䭷܀CSVڵsZ1mf .`"\y`5:2iyT*^Hl뱗SKJLAvv 7OtBm2Qkvo>x*ū Sƹy2F`l[2>f-ĕ7ǪD+0Sv֬9K>TCFǜcU6Lۉ_Vw朁:ףxQ]9 5Vbp59#j\Xsf)722[{\owm}i\x՞h9S#ּ7,ڂyDKGYy.D'uu{l2N" 1y%-!%ͱUv<+y'Z_.zޜ+pܱJe{?ڂ疯 C h=aԫqR ݫODB"|q8S?zqUV8p,&iq9sZ!e]:h;N8'Q,˘.!tUS<**-{iNӔBIO[0ӶF=tMtWGQ"<ޥGHB57/wLrLimlD7 Mm%;Ԝ@vc^Kpqts&8 c4 IS 0:4;7159ƌn.eS*F9MGEVّ8$砌S9IKq3J&̏yP)Βh}Pzp-dn.C_!iTck#ǑYXags#/Q*Rx?inۑz*0}ǰ~}MpŷEvꫴv6t .r_]A}5t<eD" f#Gj*qJDԼ1RNhНqҧy 4ڑ%m0%oQnR^pJ qMW!=ًn:4\{ J^ݻYT,;*8Wk9lHvmG yX8:<*md~STI*1n}{/aFW1!N8?/b u#hJp ]}$61ROr~XV ^o.DvTy6b]vNCߚeGndG͚+ӚQEIܐ1^RS׬y>$S0`7cpuNf^`]0Ӈ@/xSu5~cgo\`{ T lnW N|V#Oy c$mWK`2gȩ1u$,:=m\Tr0Mj,|XtE܈ UpX>@E^|x-yٙTއo Tj;wDKwB)jt#W KzzcAbEfZ?i)Рrn@LNLQ Nk 0$H䕩}Zq` {h0NQc͍ ÃŎ#gQ224C>~Æl,% h=zHmҋrg1e5rlfӢߜL3M/X^CˑFEێ0SVS0ΐsW9LQ{ׄH7Fʹ;_V%47οalz>TC'U,^-g@JhsW wKEN @.(drTz{ (JKBrs0ǛU5عM2fovZdX$:̓<9p^%chٽ#AƱUYc2—]?qâ38xTy=w29(fd"?Mю3O;w,نCI5dClفt&'-Fhs t]XsA|,u]v(u~rYxwX+B=|$ь[xpQzTm5zQ Pپ9e: 2MS]{K/ɡ24OF92u?h=s+wPzXW,8wa^ .a1\B>2&*,ZP 3COYfvH"UI"/zE^~@OS0$ya%-MxGC7VcR8{X<23 Z% (^@l;2(r&ǪȁD&z;_7-.O\\1JO^ ɟј֝/-f]K`, -tؚ4;N$gcxv0]9u2L #dgslMB85ظ6 < oJȮ]"ϱuu1é`I+ѬR^1z,P[ͦ(#ײ*vIvS~n9Ʈf]7)”19g|r'|:i9|_-1D]T]__zPVU ^Q}t;⨍[c>.XQHNF5 qtn'Yu٥x"/1E^8o(Z| j@.q|MxqunfM*_~k#掙QC_.=3|<*ڰ6A ̼f<+=hϊ/Z{֘.|gh -W(57LCG<2\FEO=f,6j<װq07^5[^P}j&gb݄{N oT9 _q_zlE 4Q~9u4Y ?lvN'x6=rc`_{lMB%Pۣ#eU<:L{Z7c\[P%S"~ﴟ4zOTKp 2Q?__',nv|[WaFݝktzWq{ʰP3BmƞZw˪Xќs,,^y'oE?Y)-I0$cQRje* nѓGKd2 M[5i"uI:ˋűt@"ݙSFƱղ*MQ,/yXgycnGmcK,036/JtQո|a]h[Zkp*FfxcIa1yMcTx9%mNiF^0 y%Q1n<&Mt5y"**ƢrBq\*7J|F~76C <} u:D]?M>\Lٍaaųw>7thU=һGS@~w m0v(5Ϣ-WFCgyy aܖ$34DZO2ϻvUE]@yhTz90Zk)HD8u'^{ѳ!?qb X*Y8~]3IsڵX)~f!J3tTz(ZTIimq |yaɾS=Us0j"9hkEaOrI y' [TvK,Z5ysPefy;NneBSH:Z,kq$fyNa ]b~^Y/YIÃ.ŀƏVͰa=ˣNc ?ʏ7F>љG`LAęDZV}(si3lfcz6?Գs|uLC_GWdxrVwm0 Z6F '뱵|xlzUDp4>XUG |n]Q ~ٹ]o ^ͫ'*BSc;T(2̱J39)tZ#8=g7s=*] *fv*uxQ l02ρY}guY]_u.vc? ڱ(᳚*Uygn>sf6TZjfi1Ujv}-G==_ ѓN`hoKa :WcXiǴQVc&NLl^~xF]dWk?O>k{je46i6}+lܿ55۱w(1t]20cW[M:e.2$2Mvj95ͲZމFOɇ98TZ^WAI*fy*H bz9$Շ-~B7OLT  AɴT-8:hi#CM;}^4ZJc}U}tofyV1T9`r;^Uv4k⎀̩ eY5.蕼U49TycѱFE(='{7[jR6?rm]Y;[ZN#ЫqN#Xztf4Fܻh8:|a!=Uv7L}T²-9?̘: 噭^ᆬO: #={D3릦1UW㫗BgNӽK7ק>FsPGO/K'{nG{\ftYjXoi}8}Y;˺kW$#$ CJopWݵ0R2Fɸ%yVwfK,[TtOi\#9u̅ _ $VB&t.\[N66/Aa)E' d6kOduKUvztOtRʞpUW-@9UlO^ z>OLPsuo$;^@R: `06໿S7OA^>܆ ;;IrL#p#cJ]|O}Y;;ad?t׮_^{x \uM_u¸KYSB1*DW1nVHf6Ͳ3"tExP\u#wpv󗲞^ &fYuCK]d~}+{T z'fJn!Þ'_ĸ3jOߦ,ZQ7^!_ݑ< 5VuFƶ]x}YCa~Kv4ΩTFdlS9:<5pډgy@B"sʝ>ncˊF؈!5L:J鱐vho/{zeģyss"+ #C[=i} Y3UqLG2&<'F!{^{n ~kwE5yŸQ^m|kAgKR+9H*4Tvx(؝=_s3Ljpt_7Mu+ƥ'11aB!v}.'aV`:eО2>g|޷Mk܊b'S &/C!fþn]Kiy`?ܐ 9."SaBg"PC|>/T\᜺N;~*95QAvEo CV eCaz4b$}MXn^y$hllD7 ݘkȸN2Wuv,kRB<X 2, ԴX%ϱMF_ cpI5UZYmOY"sa%~Zӛ9r\637bO7L-{F'fx藻xt|퀼Ďʳxd͞2VXAЄ>1Rco_r%(!Wݻ^DQH0DmWvNCߚe2өlpMڰb!jg>7cpuNFG99z XOo.Dvycģy>FY_5HAUn8`{(aHsp[olTr@co3>iuwH{|Anv>HٞQyrvocCQ4:A<3؞<8%ڎGbuǢ]?.憺FxuǷ}[MFN}դ.V7r1viYUweu^#pspP9ղ[b>Mq,JF0<_ \4n ?xQ0:"r^jotb|ivtxfUAzlg/;t>NPkɰ} b+1xamnHK2֝f/O :lAl96ґI$hŁHia"=f韛":{mQ k{XD#M 7܎{6oǮбFasiJxya)tВ_h`a8,mA!t</ϳ K9 NQqDwTh B1۰f'ggtX3&ǥӲјw[l-uٮ 9lPz۳4sPp}e,S*F㹬kAt4G~@4o"ލȍ^˨:| K3 4/!Gts ~z4݂Vl"mmHD*w{/Ͷ˗iu;CeTÔΰ@&863l (眱G^9Q|>{sj<<-ƮS 6vg0V0bֈю26q|f99 6+&uI;o2))PrNy2b0c .;Zm!hS~g_@^Z ,N3>9XuڳftN]Utp u~`},p8^q~Tyz\]_6ſ;zVT-Wٴr5vhmᒖ؍),P7Hi~@E/5҂Ml;@ vwt]g&)M\)IFˌ; fQuѣ=UbP9 C҇`e3!6;ٳe]GsST' ihIo!557a ri}+ГSk:us]9oX77f걝t?c{\߈]e 9ѭJpa:_qϲo@)Y4;L0?4Vy.Wn=C<R ːmƚ767 eCt[vSx 臞o\$S:N)Xʱ>NJYduɪt.WӪt2&x6n3f8x|J7@`36FM߼B96%e#1,?kx$IfT87~Ӈ?]9:sLp,LlZ{gPvl%8CF\:َQq2F}}B(kò>1+иGfF~t5mxF۳9뷠`C JN ~ug{Cu5:sjP4r7ޝ{qV ,t9&lP8F{%aGT򷒍M@IDATݝ5vNg)t]HeܝS-voD+߄-IZ;E!^j;Qe_u_gÆ^/W u SV53au>M)}5sڝFɅ)e9/Ax*MxJGK[*A}) muX@Qd)sPQ5&cZc6B ;Ly|=? &sՍ==_N_*Ƭ31o?2Hn19glIkǮ><=Ж H"ܦai(˥~{~B|lm='QZr?1ͤv hޚs,X0'.1n):J 1I&9-cF%>;pnksq#С >Q Mc~X2 'v6ړ JMЦi |eBWcU1;BBI4}gbSȣ-bg?~!u0&){~$\(;h&SZ`ZSH:ʂ:V %FSc䤝u[[UcT 䘞C'8?y]s/(,_" bRiGքMHWOl0'N#qYzj2q_~*A_'AV:uiSSfGvz4%p_N~Q{vۋ#yf.-|1K ctɫSyX>G ^i8V:#NJFoquV!کXEGYұ37鑡] _nJ7(neS;jjYw R@K ֬;]::/Y3=o4Jy rOBQBOl;uSeGå3+QG˵Ex\.aD;.͎*֗i?ܱdgғ:xGWF&4%vu^@S rrysuħ}^ḱǍLwa(=Zk%3NC cww`6\ۃ.tcF` غ-bJ!t+wq jTViViq,C&iL%fq>ռ~1=_N2]B1=Ö wQ'{ɜZ H0>{,%JOnFBE`ԴtchQ?;8_bi`5i*Mjf>BgNcm;vAxJR(=;B7Ig;jJџԶk48%/,rX&i汳)yqZ ,CG@Pa_:E`UVfh4F uGT;2 x2{hhj.sߗΩ<)iF@#$ ӸE$IJP;7·s*Yḏ;y9e@~:h4 d^ƙ}~՗LU;[f O 8e|>M:ΣxQ_9wGťy8uʹ&kό*NwiGᅧ-u1gd-v ]lr׮B]/\)E7R;}!^z:,5bji9W^#0G6[n6׬mĴ%wa!x +84'J7!4?c/lDշ|̺r}U|f:@)AǴ^>ZB#81XFL-#y569C Я=D"'s*Sr#Civ{@>D9&{.%֓i#}i8s!#>ﰯ:I&IKdZ ƌ6HMAYfw2+Ho8q[w7u?F{Ӏ_>R TߚY(!1jGJ[fSq)}vwH\h.#3FAA ^!ǔ:y/#ô> ~^MN?A;6jF@#s΢j{_ DtN͆ yy!zXFz|4iZo#_drnd~*#>;JUeY[Kp=ϝ՛[fg2)>yhǤ7`rttd]-OqQaZe=kԞň1|4!ڰT 9GF@#8Do u}@ЛݛuČx1<@M`45W^WaꜚbP~.UX4%Ӱ_>~<5b͊<5/+@WM2-O zڃԃgoY@xD=O2^s#.We~\$h/5$+nozx3GfY]u5]#IZŁU3)u[oPrNcj BxVSZZP ĔBnhy C iɭEaO=aeG{nmm_AF9FK!Yf墽U+̈́Ϻ*w➡3|c9I)[,%4uHL}7x׭q: 1 %9Bΰ{POI4Wo&B(ƢiG@ 6bb Y|R,IێЋPb1S9,$Bwt7 ڍ7ᅦ\S5l5~_on]xIЉUh4!v`]=JS=/vN"drbBQ]6lu<>J|Y(-j6y~qEPwr ]Ӌ "'s/װ)a$cf\O;R_; tgůL Žba_-5h[4م(!d׼i$G(p^j/^َ<C,m38p^&' FFo:NUEy昃9Vi+Sα!ڠꉵZUtM#JpC&5mZ{1zaxd4n fL:L+3E29iuX2g(MA2/ uU]C'WuDiޱ+cdqbGuӈB `)Q|,Q10V.1~`G# Z\#h"~$H#VXtCʺc}"ڋ 06mX YOK#gSXzmVy?(i/WR谊=Yӱt k@2*| L>h=OK8- ]"bꭡXYI xbX<2^Ahr![NMՕVkP1CQ<ZT#H1p R[$9UZⴸYbƼ/ZCؤ y]Rec_3;} (VY\a2L ⚅)Ǯ>2'<@~nl"7/'rr*uu`Nz[iқBlBgA+[r|4mו1C_? s#vHƖc4:SfWXuR|Ns'䁼EguXDA\4F`/7 ǩ*$'<|,1`gZUZhR*4v~EFjisp cɫ1yag%7'^/Fhܘa04#3aCMX4E4C'LܾȲ%A5 _&KJf1;Z9wsA3_d$-1JcXӜ e9xVxXݍnrQi~Tz.huQ 1e>A\5fndKFR'RurYh4@E@&bmaJ,$c{EȪ1Z6S[h8ZkOkK\wQ}iK#H8x𖁟,/w)| EO4ܙr:w2fY3u3*pv]Ѵs &-tq"`sِ/F iD׮H xC,S̲*_r3Oh3rnfȹ2ռUUFtj w⫲ns"-p!`sMڐ{>wj4<[Oh,#i91$o0^G ڍYΏ[CT}vedѫ;vr7,o,:SPi ʆs<>je%H9e-c C-HO.׫%5Du9 G>}] Kqx_ڰIUWc)я~:i{ZIYwuGw݊q)S m'^Ūpwĝw|Kh!_eO -}܄YP_CuL/z]%#/׿0 +C*HŝKoǔb/v,iQ딴 u2RTSߏ;shH^uޮk4 ?Hکy~;w{={)AĖ(:<2LUV.J 'iħN8v63R>?Rم){ʰhn^.RS0CYYVVۛMNŨIQAm PU@KCO<L7'ge o j~c}qSjFCMK|_3ַ < _n0V*{icuٷ`r{A<~:77j`-qW%fO 1UptR#h4&%%-=jԨ[Z[[yVLfPU'SMEfќaZ 9߄ 09/S ͧNzˇxbaP.3Fx){rjG'1cN#xѿmC6 G#r؇}M0npdIBpdARI~,7BV4F@#𘔙jN,Td(r0:jy҂ґFJ( 96ᅗ *ًpɴ5|ϙ,~Z%چCx u3磰pN'W@CXW&zm甩=L중ÂPtvv`ȁʹYV!1ġ.)qt;عp"&k}~!S)pl_3jh4AfRy" u|[00xl |E;FqGg=ib¤97S֝2߻?^9{[rJu9 O/CYcgj`,t% L}}24yӛS_o:8r❓cթ5tfQpʠ = sλF@#?w )eqI)pV4"pdDVzT^Df,Be&oc~X #1091q8+Ki{&Զ:]89NKV5&3iW?N霖©H!wO ZM}56m-)C7$l:y5X|`;M*[Noe<,حXvmvݡ6|ka.F!''hfS>v{ؔ&AI0d排r(ζw֜s%?Ƕ1MaCƵ>8,mRw*뜌 Q9]iOM7 H%J+1d_DSMֹ9T~kL+;<]IX 7cߡr.ZNWvj!b z&"t])-sDDjFW7>zq䨏dB}9 ~O}ӊ(S Jpq0Q_+0H+cNTpE^s7'a7(v1:=4JI_ Mxajzmji@e͉7|2۲7H]skOPҕ*I*+׳ޔmqXsrTh)!*SֺkK I?WJ`-D\Ai$K[+[r`x/JmRSI8U pʿf6ُ)Jh<YXz xq?&m[-9]}.Bx%XKTI?@")IU` AB`9vt=s/=ߴajNJ8ms@ytz(9:,6$PWSTw9TPJk*)rJUYdIFG2 iPMA&\vÇ|JZ Uv}nj-DHB,LaBƟ4讐ɷ_IxBW1s&AŁx18{0FG7S 9ŠWB!Ӈ隱qEP{$ڔ3yn S\ie0=17i%^uOsx h#B?[#[WP9⾡6I=P!r]Mׅ5©*zu9ǿ%ގ  [pCOFKL/蠓a`BG|o%sh7`3B'utyȊ &8 DMKTg1LE,Z84qP©Sܾ7yq%O-uGB}@኶w:\W,k))+)W1VrśXC*=$E0}`\rai/gZHiQ0=uӿ1Ñ^&-#6! 뎞TKwB`wм4<¿BƻoZ*9˖̥SdaA>] p^=Έ0jD`zQkNʹJ&lsf;PyP!дZ0mFXrǠegҮw$? ċTLB6giQ@-SmҮ|z#6ρ#Ig-`\wajD6ǶRãk8`њVĦgюR:\2BJd{FBgG3EN3j*J"P,OL1U>:b Isʟx 0=13}u\nXjO!7t W^ Ncc0 "渴+_P% *[t56"u0Fd>U;]Ү|I@cX*Lpr[V4+|7Ķ"iW$12]5PY 3  ,ݲ&s_ͿcGRCP_d摹urųhyP݄&k5竘y23)&_YKKult%,fr[C?؞Fi0%XYigp52YN,2ҧ T*= "kJxJdUkL .?Eb27?Q%^g) c!\3q0^2o(iQl%lBW"P?`p͝HJv%1?JZ0eHeVa}pi<36#O>6fgkȇ3Pެ0z<\wQ4r#~ovkϟrGQ©,Lia߁}8I]1`dHYS {YO'v؀z꼀kV>;vJ§9Ú,|./4 >pGwV!AKx2cfp[%%pePKq\z\zOKᔏ$sU>>XuIٹ01 #pv7 S;}K8ju^.6ĥ`ڊ5_GNzNָ͟Id7K` |8=3%cm;Ö4َ/_o$C!`*#8č6[IwW.nv  X`PS_n\ 'G.iZq/|ū[ ԆY:T2~?_?#p_['!&<[܁]xRr5h)DWm4-id_X=g ןHC p:;s {9\(oˁp3E/*;nPfF0Y۹9騣bNQP0VYbs\6p:yL@'bC^|z>"$k("FJ{ZÕߡ|zݗ 3 `OMrZ+\ WaXM_ :/"v(?S2Y Upa2F+-gH+ZPdFdRNƵ7Y$[(sdA0q͔-Ls9.vyfToT"߾VN瞓 Li;^?ៜkǗb Mƥ ٯ~e>x%?C15={>9ĭ@MBz|>T(L9{NoD<"SGc2+}uvɂ"HD&w[$:TbXS\Pz\"K{uW޻i-:pNgjW`Ci{#)e8Lְ=>3[Ӂ%%_U|ltu 2.ȡkܧp0n+kː}f2r˱]'~f&u(o9rוӾ: DR=ohFa2U+{f` 4b V[zƍĕ\Vo Ow4) #)6 EnƗ5c 080 9ԚBn*R%ZU^㟐pJ& K@?b0vB<X) z'6[. lMXzj>D}a\R{6VZH*pp 1u+p!F)K|!;q aE{J%8RF.AH gF 8d#ye:8Di AU.`"eqoiˡI?N 5]GMVZk6|dێnHh:܊vtՄnj[ |>Q14,1>aqYXYqo}7iו1smqcTq8od;>jl.%GK$JI.Okl׋c.nFCS 9v+   ?!rZ+HA%aV+9-(sdS^JH'*YƏE MEXAbw n ;R*UުOؙU0Ll=r\88+1FFgOk{ "O/Kl:NYYnE 18z.pSAgao *lyZ2,Ƿw=G)X}BL6ɨ0D4F08`pbicSvWDNr %L2-(S|Z Jar³ėW/ %62C!&:YLQJBSM؉Sxp2%J l9vi5_R>ozo:h]IeMq1]9⒥KNM+/{KUPf92uPs`;6bEN9tY#1~heA-ضG`"CM`Eఄ,0zJ"mR3568kR5ASŠ@ہi 0808L9\mhF])2꼨i`sp& LRՙ7(SS;xsNHtuaJڣ1nRD|cm8WlRk#t n(Yg?YN#%ce3Xas"/4|TM0}Hgai(+.CH)òHGRrM3n&ȑkQ}:qB2\wc;CXI̩$Qe+\qkk^*i{W!wXkRd͹K(V}f(48`pTeo8fVuUu:.p*Q (9iAsS|.E*ؚR; iڀH !Of= )`Rsmr"+Fj4eH4rJ0P&qh&4,uk+:IX*PZۊrqM?>Pzq\LI>w`"/>&Z@D3D^(t q{(@dwWYb'/bH 'r}GQT5"ڝ\ߌdOPIjņO֠&|6z|wDžKl%PW\$~ѣs1&:HqQv6TSX\Gl-ѣ&`L G:mBtګoÉCOkY)3Ǎvlu{Ljz9oKpYMyLЃ㼅[ҷq/A*t#}$n4ZGtE.vh›o7i$VeMoRkmza hDKjwCm 3!VlqwCN2|=Ѽ\A^ ٨)\ՆY ލ Hnp$jm+d#ڎ#%UФ'9&k"HUs,t @Tm[o5Ci䌟ɩʌp^F<6Yt0*i΄T pp=`!!)&Q)S1U ^HgUiAQ 1g68ЃXu\~ AH\/q UJۍpQW8hCsYVE6҆:J=9mP@:_ڣulKA9o.kG E}44zm0tjɹ/OL$7x|TWɜZzՃESXi KJ2<SȌit|!Y-tێe.ç+@l#21.Xį~bu5aR :xPoxV1gF:ǫ@Οwx÷b/vsޓ?]AS%Azp4+p` {296JSDO1㉠,igO#L T/~x;#6/TAtl {(,Ki* x67nFkvbEp$"!Dm>z]BzpFk/EvPZBIO4:>,QWxgUaT-HLyotmYQq&!zgU\0oRh>~Ų=-'_CI@U`KQAzBl*p*#k8^4w!fN族 6ᱏ=9o1t8$b́߯>B±%M>Wz!>dm3$+12o'̯KXd:5eZ#e\[=W3j\v^NﯛSɷx^hs^a>iܐq=p,#ӸCx,V,,*\8淙ȫӌA"ϱ"y@-ȧ/S2e2;QGj"w[*_f:V^/#I{ZUIƾq -sF!$0 lPq J&걸ߒ ,w0kL+RukࢅChg 9:e4Ȥ1Ȣ!y8k,Kj9LGIgge[޷9wf>F`aw6ә&y~{.z9NgOX{9u,ny9(wEd)/)2~m(>}=|%c1kUX -_QYyssz4'#=yH8,wVIWN9vq>vt?e: \TѪUB B:9k").^Qdc憳~(\?Σ dC@?$"+0@IDAT/E^#\.1ev - ӃQiMU肊VڝyYI 9UN-B6l_l"e娧7rU]+haXer[C(8چ!!HH$U3xbQl+[[`: d' :|.EQu c $wǤq9;,a^BY0-s|"o4*ErckL9_5EHX.29v51]yWDs>{r|XZ9@Wm̹8tazG[u$"Cke9$xin( 5,A ZGT3bW "/fc:r7KN_C8= 6HPR.*p cOPHXMX;2gMԩ6[]?^Wܵ;@58h;7[z)ڒ| VxB GY?҅U.JI襧 Scշ<yKe >7iIi˚D@L 3yPN[[9>g~GCd{hߞ HаhcC0A"gg_ رbGZyF`wh;|uG G|8 5DZ@!I !"ah:=B+'u熳~(ᣆQka?zQnĺ87/.J Ap(p΋4+sƤ!ķM ()܅ϋ]/ ޟM-?'ާE&P|F#ϼ;1/_LSL'? &mQK}, o[V"[iScƦ7iJQVcZ~H!cɭ=qXg8 1YХCMF:;v9rIZ51>Wk}Jzz|U^.3R̠"-cE9BLh6Ɣ6hEZiKM]kn(q+'q")(]sr#~dPKL\8~12E :pXǭ/h.ۻF&oz{ПXs1k~$k.B]pX'v2յW E5gNDݶi'q7b&ZݦzKw8Ax]AU:$$4 b-9sIP֎4ShNAd CcB6Hz54JL%bBe>JTdlC=DZ+S>%[OsL%y5Nv[RezpPҠd߅ɃRvJ9z@vx]]/Q3vW39! I|:s ޛb(MeedHÜd܌܉ Kg!#:JQ@Z}>isC˱q6J0!V:QɵQvtH K qr^՜UPS*S?ֻށ8s,Z?`E~+)4g9:>ןw7Hs}?*Rm驺dA sAҜ3PA:"⚥B}$`6ZS{-uE`5qS9r:9Li.%hAa/g["rrIK9,q[[^o-~iG;m e@hҘۈ϶'gNbd%̝丽{]si[k\ʷ,|iڢ1K )!-BMb: ;\v-nAfNDL{!yL=VW{2zuI/D 44\?g]/txyr|5SZ^JJA ]_`eaK9[l|<C郰w#,Df2=hcgBtF םȝ9J?TOyM[T/@/Ce`E^Č/X7i*c3O^ypލ|]d޹ß.?r[}y-EzHfIGȏCHN_FKZz|<$gc:G.oxHr+"90W˥^+:&%:pZyH2:Oe"9kaI> )r2y Gsz qH/Zm>/wޱDfja_#qϒ<!2̭kꀏ?ڏm$P[=/D0+-75<a~VtV?%^>(S%Y[,`XN|kF ^\'Y IH FK]5Uv0op>~YiiH Y8d=8)?=;Au|{n-R 38W0GЃ.:)G$j'rׁ@'e~1k~VUq^Tq8c%L=8 Y\ߢg)8sMO=nk$6mEfKŵC`ywղ6lʉK)y9$[xfӱmmcJ"_{LT~HL(|Z:?xz17&IYnwKǨ5 ^u2qFN6\G @$s[QICIi5Y%@?3ytz(9:,6AZj>D6 2lFw"*:1lOI;Rpmd5 95rU,HsZ~A99=mSi`~gm\~+z|ችk_bV:Ͳ(DZ/ks76.E9C7}vbҩ.Gvv :B8,^1$Ibg(G_$elik̈YT%Ԥv,w3G /wH@p\2|1THH whƐp*3f*áĦf©Ow>LCa@ъ'?aҞ?:AN3B?ۥi8/ێ&=V+|y8gѥSB¥h8 T`"))ӌ+i)6:|Ɖ. W>_xT϶YOH=SOE[w!HJ!Υe4W? 'XA&[&=DT@,*ӴΎV s7ًKGB(]>I>чX(. )C2:TC$i!@\n7:?Z?mSܾ}lF"4ԟL5{< 8vS ¤u|i)TK;"0$أ!Gŏ+n zZ> KzNU+MU[{%wR<؈cʜY]͇21Gm.Wp=Fh]2׭S#,6{-oԲ_"%vwܨMdr(Dj hIR#^: J]<28rZ)(RWJ_2ݱ@F]2y&Lrob6?k§^`|FE- /0q8fRpO<-1M8o͘/D?'cMX7\s%ߍN=MR`JGd$M>E(_I;e$ȸHZͨ9lkƈ 08`pKΤ ge_Ģjw|ZjN+oމqy6|m Jl_/13hmтwP(A>&b$$cݴ9wpjݍ%1kPܾ#'S 3 5[_y+*-4L7-A"<6 Nߜ(" `YL!9ȢzKsJrVr ?lqYXӲ18ηo>= wY]~rguh)7≕ -仱yϝLٵO=ז>%w`Oqr?:{{})T"_ nͮѳ{ڱgbטB.P^Ũ$u"k^ ?\#I)&s2Ǐb!l|JL 9 g, S:Ǿ"e$e+_97 1̮UQ -QkX78{Y!+9jj@Նϐ_ڝD:7 .)7p1r,KU8(Q1ՓEVPDv>EF<9g]Og:/>zB:H5=8J|( )j÷G=aBe\ SI ZED`%m2_ >S3b xr,~ʪ z" QIkvS%L+-`iـ:-`4+&EcWƜv;^^5uU0?~p}90+N UǮoTY! 9 Ɣt@2&tB/? tb<\\ c.B+^i Nqi5հ]tJMxc([%[f[3y`[+$eaz[m2>Q-ɑԞI6Ħ xp|"}߀j%^#9oaaw6>V:7~\sK­CW^{2Ҫ^Ub<[uMLXXe=;txλS0Y[{0$ q}e_ s'^?Tou8 "ͱ:-`ʘB+ae"(՜!Sw㆓ O>$1"s <+SI]~ ]5 *ˠ>]ż TҎ~Y-{A)MGS9Tj&قz1tƒG`wO.kG9SRضoEE&DN8ϟ1K>:3 OL_$2_t7;( fs'5FE>]PC<;LE?IȻ;$u[iSQlg3GѰ6l$/3 dkL{jSW?[>`b S ז\rWej\{N0-\ˡ^6߉-^X`!Zܬˑh)B^n{ Y c~e,͘l_+j6>39Cwj<:Nwu○ f][q:sFNJ SWaleJZV{Q&ء॑48`pqLʟhX%\WF0w4t©.6H>~RT[MՖ>Rnj3J߲'avd~nk<|/+=8*;z|:iZtV ~љ/qRמ)ldY> g 4=8=MۃжIlA)ھT' u}BGԄm5g.œ[Ҁ-+[1N :hMżD&d"/[@wmuuq鼙^n\ݬۢg6aֿTb֗py j[ӉylֿVO\313ЊΖQ{?l1r'9k~ r wyćBʗ+uD4_̼K_?|G$wS_;4<#w~BF@Vݗs¿DW8 o8> >8A~/N:B2.矓V;8q >!gA'`EBްw#phB vcƱMx죝k-Bv@V`$ (1SlK?ґ|>\7m"?4+z-{LRXz|LH;7T'aExkq0xx+x-i3:nğ!,8sqÈDi/Iq pUAln)݈\~x]CjE I8GH;Րv{E̟=6uj]a"2 jXh>8ŤK\DyBU9e}<'CVd*OV_O0/3ƧKÇ0=8vd~AOva*ql^G+aRڥPkkx;NH08`p?N+iuOK463B1sr0gd:PWՊh$省 ظPfb,HPU ߨ %WÆY 0Hont:>ו͹Mc ͨ>ZAtȞ1},k lM;NK#/tZfjGbJPD"Ug xh&mҀ0*G>EŦ7wk DtXH`~H̚H(ɀgY0""y_xsWYaĹ7? g]tQpQ=5֧0ѧD5Uڱ嗘EUꥣ+?B/ᛴ3i9B YII/4=8*dGzm0f\ 0ཝ2,9J{8p5y#opȁ%QJB0NK kZ|r-o<xz^?apAH!>B2 ׾7T4> Qgډm 381$m͖|Wm5݌q v,/N[=/>o H0Tӿ)-f][ɒ s<| Z%ǓZ}"20 ;l&P 1G<ǰ`b" VX,kandz =֮2w`;=gˀl-:qMA.ݽ Ѓm08`p`p@)*;pgeJ>^o@]6r(iRW( 'ߔ{OĔڥMorIj'F'*DR-A~ҢʕT+fc/$K(K >HHu"e_[,bV:%U.|iټ|_UNR҉CG 嗣SN{FA)t-"עzVgF =U=LՓ#@҂O }~>W+\=KX+U?\TXՒ؞lszM 84 lWq5.V!d&D#d:Ҹ*tʺP4CuiÐF6j  9Bq_8,pYZp ک,$aat LMl0 .ɚ4Z!)3jΘhZQb/q ,q8fFrT8RT)Wmipjn[}3mM%IER5.SD\}ejef>yNp#xAg`T}xD  858odm@kS TA쏏,agfqH ]> 3R1K )!uKu!޷uZׂYR BPL6U{#d;J&c֠,})FO$f AZ?,+ȌQU[9[1a7v3}dǛntNNo-(]5].KpxXjB)f.X0 6֢mHJG uTu@#1H 08`p` qT^^^O[JyNm6mữ~.ưdL,KmD' n*2yM=Xt(D\%H$+F2$9yS%h(Ce'*aCXL\I!{}~ka.F!''h&\>v[\M/R#h9x( 6fֵ` ,g-3 -&:($F*ԷtLPvVffZV:.Nx gAsmW6i}J zEwtwÀt&AӖO-庛sEM{S xHqߓLc~z}rSThJ=ah\MM9ZyX'pDZsZ7s,ʘӼ}:yb!oQSWƂYiiH 󇹥Jmrل¨LOMQ- !?!^30].'I E#*=oKQt|xCLv˭t pX'%EМҋoo3LQP_V.JKzuy]o1f;FP!~ =_z: їfniKN_֟:'+;J|ꝝN>QL:LYߴ9f๏G& ɏ]q?5E8l}n 6c]NZ%aBVGB:V8-~iE^ӜV\Bh%OB`fCY~,9zIS*Ϲytz(9:,6@͆O-ú0SkuznSSwV==Y I[xqԷoX/٤Ҿ{ԞUAŪ$<ħ~7sCO[zpG+_W7[hS;D?7ӣ, ρvMO&հHi x$ӓN%- #0$φpd ^\ ǃI;)] 4U1p`s ;UHc9*mtd{y4D3:p^ьFloɣs 2{47US r 8|t<H 8CyJ۞| g$}jg͎l_<!V A>4SKxMiyCڬ?v:#WM5omSqe&Z,Ҁ܅G0(rőU˧eȽJ%$u "-w8N[!LqՃ>VnЂBG˥7hڃ Ξ?l?S7iV|n9&<Y~@EOU'RX-=.dR˵h{]d6nP,طݽVޜLwBZ '_o|>/G?w&R uT>"H|]Ni`km ^B˂炛pǒ`z`2*S?>ū9r8z,u@)F",S'Ʃ]Af j,j.UR}M9&+O?9Zc8D鰅 X::.uVʛ[vbZ"jq5`Z[nfF&fRFcr^h䌬H+*4 U@uj|cG1S a kOmAo1nZĦ4IL/s M}NA_'v{#1$(!H#$C;!l)BVGwiQ OkǓ`jEEe|1zL<5B⏤%D߲r( ](>XFG| F qc} I`q N=bBtTF%w#* 5OτwbH4D$ _юj8 . RcVz9hRHu;4Z#^#$C};&%пpv#TW mhWG$F%v!.\E2_x]&>J'6>Zl7 iN!S*=QkPt 9Gu7Q* tL-s:P)bHL3LܠzZυVs.[0/7ޜzEYϗM(/6xoYM4'ޅQt[ 6?dSS"^{ddWY3F}'yDN5_Vגf=rvDy^i?i"A[nOInbp4>O,eX3YpӅq•osGiqƞgq#X GѬSS/h/*W~Ϛ`xL_dbA␀)oƌ? EΏpk.}8$lTTa;Jǯ}W >Iڢ!y |!ڭgAXMCKsdoj iX,r?"!vȄu hC^aVp|ܿݏ']LM(gf!ܽ~|.8"{άu)k#4ka >uW HZtϵzہdI0e>F`bϼ8胇d0Ͱ[,OC(ݧ|&I߬t?9g=@wぃ]Ƞe[J?}D X7rb(SJߋa88On/ Sl42O8MjF]0cZązv̡z2&ؤ!:xWp;7\5Iqm ӁޕFQdor'$ܓ! IB8aA^Up]]]_teUP9\i VLȩ`ٰ"s`o rMdz3gqda(ٻ?pՄ-P_w )5:G}NDc˽R[z :YЇ3/`o8Uz(K}\ƲW:@S\wC)o"~p+i x5P$PTٜpia &WHw\ NxMA5C|kpCaKy]z"lqt{{r@tpU]tRWu[5 Ō H@Dd4a!:L>wX8^a@W8p;*c堫 g Va7t ,8̗%/ c?f"msX)3Fa 7c&` 7=q1\Fmѫi>1gSCnSJF?FqǨj"q"Wq;} 7E0N0hӷk5G}8C*Y2y[n{C,/86%2a`w"!sqj-*f庛cnߖ"\d ݄([1׾Kݍ:b 5g&6o2viB|B/Zy6m|P ^_JjN|$#j3"9bPL BSabRv)ZH\{ Xar<}4JSloCfHI\{PcZ&?nVV6}$61-KG`UDio\qS\5"鱵 o!Q/4_D%I*=&i.ӻ܀zAZUJZD;ޡշE^|;YsjrG%j-)tDgo,A#"܄1 {frnH˧ w} hMZǰ0wԉ%^aLP$o뽡;͖,֯^N"ӆrQ($N2֯ vgGWϬsĞ;P=f;g̬RNwC 6(EpԢVa<v($;~% €Z6O nK5r5 ~8+IܽLi9nvSqlz RWӓ#,Q'ŸgPK6fml)"#BhPFl.o>lJL3u!49a_2QT@NNP dš)qLL ae ,_|=ü46T9v3OޑA. H)8 ;#TxzK>/2BՅUF0;ߜe0|va2ʴwC|ȕ17X "7|6 GGӉ|Pέ8 7#ʩv(HA1IbU>~%Y}"U"\{N|oSy4-$/&a#~1 /":+tQf]ߏ9{HuRq @ڸ$gřb>-2:29sxd kh|ϫ#t]Fڢ[}0mtFna,_#r[Y -$++P{Lcrk!ӗ{Cmi[[V[pM:asӾ۷ẳӬ] [! םDTXیBu9@=ۖ(x@IDATz oGj9*Z>'gsjl_iň`,\tGg};rMmҺAO.}Ԍ;g A`\8I ȩ#W^<؀rK.ggp_3R5mN֚r% )3P`,M2,3VżeOӼX&>iL]nxL7ܲ<3]T&OY]M/mh5*ҙEզOsu_6\;TYAP)Wltϳ=b5 'Hu71 24]ĹM߷0FUw ." T} 01-+3,gyzqe1qpkgÒ79A\+S}{íf02o`s)=d&n?~aW]o?!sISgKTYG=8b7T/KKbYwP,ø8bEwY1yL4bI}$GG¾(2_o3.:GA$na'ҟ}gjDMpɞ-sMF %#2'&bUmgK)ɸ=l*Q*7 reHKmAVȦpܳ|b1IoEN>`khUx-eЎ 2axl~R['MnSlmjdžzlҧ?h{-o?kWXHbZjKXZ];|LQxS<1eW5 n f( q[Ƌf&d _n$h=e|,=)!FY\ _/zc$]1-[*_uIogo3w-//\[lZv#^ϟr\GLYV<07CdԺ1o`>t$`d\=OWRݲ 8SW:!<,sS⬫Z oeVFmU̲.%      F~4[#]sʄIQV̊jOSף4,B؋LR|#J5zx141i쭺8a~;g?slC=q0! CF:YwhhaHK%r8s2rA1Z9뢺 .W,si!RAN5DNS IJz$9nEsvC,3˒PA)ߟ䟫h ~Bc&Z%1ֶ-3r I h +$;N؋yagphW+$$$$$$/u]5hN7wݜlWlL(h\sv ӼLRl~^ꕜG"YhU  e]lj3 G 1L2nf9{1ޒ rA`bᄈ hw/[Dyb3m7㴒{Όc8M0 { d N" C9yՊj9"Xs n' EIs[Y|Y$$$$$$.KNAC/o oE4F)x45s;pLQl N@,Ǝ v9mؗ)AEر#̟(o$fW)P#U{C̋ <̛ypګȩop $+1ٹ#zh8?}Yri] DU;xkVy [E`$]?73!X4ީ,G-|iR"&bh:ՠ{\HhLG-EcV}"~X8ujuW)w[\P l<`f,fuizӹ.A}Ѿ{Öl>"5"أ]ػ,FŅ]J)hiAJ  +{Z> 2?\1/n3͋el+~gO01϶-9cF)5& ըoE~{& ^nCv)[ &/dlU޲f2ҜZ5Թ4Yg A'a-aX,F6{!&7i.t~޸✆j/`v)<^.\A5g5H+ĭϩ8P|<)Hs>˞|e-]衄bUSǯ¥ 3(_N}:;&D%έe\tWڐKy)%my ~nŴKR깤ƥ$~A8 66}\gpW~<ԀlXKeD~ј*![Pوԩ@'oJKЕ_&2d>TY4ؗ޵vx#n'zm^D,BS+{DK_ڮK.ܽ+†m?ќ{Öl1*{319qݣ XuS~W]#WG&Ycw\NW=u^N^{q.>\.:\ kq^E_}C_Y8ur G~+Wɼ3XO< \<^ F`olv[ nunKDCA^}R{׊FL8n847aaXg@m1YoRy9/eD>?םm(Ɏk(|Kl(rtñR'g%W ŧgm@E >CB{ab liC XĞ Spvr?GɮxoU"r% /%kӽac[/ߋ ˏOd}wbwJiOl bz1jt)=0qp8r:^ OR9MXUэ1 0U@v5 FC'|\<V-~0r!Nl\F,ev~\"<_ X1{T(!p #Ј-!r,2VHS.`h# D:|unsCIA_72 e 7⑅1, BE$Ǫuw5a~Yxҫh)ɔ7_zw[dD,nCHxbjNvk9}\e@3"l[RJ#bRp =k]vPњ̻R-@VڍVÅH1mB}7by[響Vi!Mnx_ m'$ѭ ]6kŭy[oED γh"ZďGX֮<Hꕤ%2=4R$~ƎiZnš* -#ذ5Sz`1)E#e 2c:t$K)NM-ƫ.G' A኎b|YhLR&aV]Nx#ɱ9Wi_'^v B*=VK~g PZ4f!v 2IDSCafZڦ!19CvO'<[1&"BV+2y`QbWoi(nJ$˵gH%{c+ u#sfIJ:چ-^sKm%6g^$p.%t=:P+7eHP|ʜ_w>/wE+U{&o^eEknZ[<0r nt?]-&M׻n}Taլ.Dy8)D< _II Hk 'Lc^Pg^gEBL""0tn ٰi?'|$vqj?l\]Gsj*aG;G8:ڰ720|0OGɽոH3m)>dV?#etRa~SM} jEƄ7*To*Sbf s\Y= X^<{rڛȞe _Z{y 0L'8UM*#B?jpmk)qW"FB@ )Y2y[{C,/86%2a`w"!sqj-*f-|t7|C5[T/L2ote =>a\2z X9Ƨ }jL] kVOIӄnz8 C¶3UKEm9Od1C+5yK;yO$Aġ/>6d&hQZFm֯Pe'~OAf`$46̞H +](>h8K-McF?rWtzDns[}1rGcKaW"-Qgs49`OA n "}x nD%7zAZUJZD-lB3\vC4D|brDgo,A#"܄1 ~fܐO5>z~7ahU ;вo9R'.R_{-)k뽡;Ŗ,֯^N"KatJP,OaJL1(lҒ)"5q(f nR G-Oa6gMg[okZv'f1KG)헟/s9o3*Ĝ%B#ItL [__EQ, ,aXim>sAjLf8&rg!M4$6w/&aPĕbz/Y22~<ơHzJ`Gmʄ0@u 9  .f:y[dt½er܊PEn}XL AW F. =e! Efpye""/ >/.&X椠 )o68$k>S:<(;A3li[[T֟佬.'v[O!.dFbLF)Dii0\2k"0i(dM*jpa4JT2M5sg"6c^5w 7.-/'4XQ9urڔΕ`1MebۊVib'>7<&nYF?}ښIX~ۊo6œr/ʀṯ.%*kD~#iˌGj{)fMq9FM%D5D~$ uoFȘQLO^H@ؙ]dC5j/uICށ.Zz)r> k[a$Kmy=o5Q(sx ily/96^ KHg=NW>/-]m?5e+C%vPen~c$! !@CG _>쭊}ߘ閕<{b9ۚ2:3ٖ%v\LylZNe._j,; zn7}GLյX(q1gk2vS%y]$8y? ŲﰡX0Fd48 ¯EOl*yg^1'>d\TLu;Q?z48V32`4]Qp5Sud.1fF-*E"A.K?!U2"x-eЎ 2axl~R['MVƭxbN @q=V̾V+pR=֟Vjp($WsuHዸf-X*}ز}j S"+\Ra!OC+22/7ZHWكӵC@W\%x"q,=’rdVR-gE&6u*byhp*왵Pj$Jy c4xbʊk!jA`/J=^4+pbŁ볝۩F]1m));T{U#G>3r ^%&hȚKGNCtdd,)6"&AD~ʤ?W'-[kL*6Eׅ WڒV}@:CeW!D ))Rp8:K~9wUt$ XI-4/{&& T4WHGe7:|e3˚+Q@;GGf,dDN4b͓vn↨V&WzѰzO#ֆz],;GBɘhICL.>2G7ՑTq/&pg҄E7Eݕȩ ҮՆ@>5aɋ솜F”\mKHH؆$2m4xFg$0wP!9ӄlf*oe䟫u}ؐ閆׼zqB"MsØI##1AԴzq |p8;!8W*J9 !q?#,ihmxkɜxh-(x቟D x)'XPY^@{)7qmaVV:# nw"IYhK8,m$$~ yO?]ڔekH`R'Ju#C j-_7N7c㽷?FN5<1xFΒb{8)4$$$$$$!0 BC֝-DQyG%Z ppp>:cb=z"=EM6} 7ݪLG.~~x1p:҈(;ІױB̛cqYThzHB :UHȈm?~ _k/, 7/U"| Ɔxcs4|8G5ut ޛ+*Pbni!xydN?"B0q++4<([.l+Ϊ%$|1ε}>ddu*iGufWV*޳JIB@B@B@B@B@BS_(e N<_Pa:ls$ʟ;{dfePp/ k"#7R3D *e5jyS`b9M+5e:b~Spoxy "RSߪ: l}w'/턝G5PU+) [;s&F^&ތA@ш3hj3}3=9mD}'im;x#$4-ȵiOϔqGdz؝E X3)e V[]5+.bi@WsmJP^+0ϫ3so@]׫tpN~7Nm_Kcj)y`ݿnG[qU~,@*8!s?Eebۊ2?LF3㜘g[qpIKS:ڇLC޾P]Ȥ@K_7LOg$ys+:+p\;ȘQBQjL9QX|3ņ"[(zp Bi[0DV?L߉O/-! ! " 9ǦˬfUTȧQ'[dzs=gpG/WpRx#n'zm^S:0gAaZdbCKZ^9^ڇkcW@izYHqU'm1)騄π9p0 g<;vDw'ˋZшםѲ0`%q{H X.v8V轡Ylv[ nun_nCA^}xtv> \[.=q6Dw֞[U6o ʬT^m~>?{ͷ.JHHC KIB@BB 2'vgs-g h&w"l[=RJ#bR@p =kYX>"3F<#EAXOtǑIԹ %!h`hh%;T4Oˤuh_U{~v jop3GfymwV\\!wrC+ðY6|i&y < Ջbv|Q5.ʠHLk5a]a F1m8WV{(⒱zld^?TJ)C^GAP{p T{Zi_,\ /gG\<.Y\)'! ! !` ZCH:.!GX /m‰ִt*40c 7YϓTR`qx0P̻kF7BYL;Kgc뒑X( ߕ.O f=ڀ(g ԥX w1 [cQǔ)=wFc9=φa=g \c0цVZ{P̽c0N3|1c*(hIǦ "3bc8EBŽ]0wۛIxmLM2L7 ;Ndۅrp?n:ýxc+> Fge,ifH] .'<m± T )OFZI]W[\:,!EPEdlT#MQ PIQ C&GVL#G$עL\7EecF34 Cx0WGGR**w0nix_$k:tQ"hXKOMOnErui}s-Pwl`a3 mu_%Jf-@Bc> opy'oK!d<{ b<hXt:S挻HFfqˬX(?TۀoJ.͟ h}_X$NAΉPV,SɑS.UA8Wo2c:t$KA R;B,dSJƬ@Y~羳0f< ۏ/(hI >x4э ]EF\Nj2 w!*N"4\H/IO xw Uk;-ι[u{"<>}4bst8"2|zl{z   K xr`7tb{W <6֟N }7ښp8WllRL,n5Uk+:e1qKY I63 s+ruS",kLBڻa&<ιdO3{ TZ4fm wwfF{_Ot󡵽|kXo~HqX3_-\' Dw$8?07geI4D@~~4Ta/հb30sxrnY}R0P͕bQI!A2Ji `ۄʥ=п<6J<0qZGcYzkch$P-B6dI&g;]5W܀24vN"3mo2'e`aHx{CEjkqf;/Ox=<FJ#UR20S#Dw"]4j0?A?MrOsŶ E ej”zM[dLQʘ #D##~h|ˡL~N?_r-&Ƈli+ꄨ@gy`VmZ.Ĕ$7w#$6"*EVOR,ski\ZGN= $$$ 0- GNZE^ (zѨar<$ې4G }V`i%Y9ď;FIlFe$8{=}VP|o/q)lȽv|D1vC4DI#SW7 KDPN>thBҺ2G-lRd}QYք*i. ܀z_/tR~<쁀J%*x}rT߫O5>z~}S#2:q -P{@m&wԢVoss"v.Ꮓ#CW7FCy=se>>=9xr\{ddXqQ)GL~l 6F@KMrD~j0/Jb&Oy Ż rjЄdƎƏ/q2~53/SzVGVýA!Sa3X9~Hc6V.6}A^(PB@BRtL [_E"%FXɴAXzćyqu g⽝9 dfLc wf?(Ku31ctI#h4^#R1"cd$E:| B`W(vvjTҰ=t< veNݐH=%WY)x rpao)$#TxzK&8Sp4$nX@. #Eߏ9{8SOj}P戇_L (Q)bAl},8~q1|ʹj$ vDrV1' W߹Ji=v[FN2]iKޤ_څ=kb^7okM9 "ßdi?CHwS׀ W?Ÿ"yHt_\$뮧74CGA엱aQJ׋۹Sب!ܾJA&d\*fE>4!Ҷ]ߔ54hXf./amyq˞yL|ʚ3axL7ܲ<3|3}7M1rdx 2 axl,sKʚ6Ŧ0ֺ̘ 04WsDKo7;ho~O $Ho𓻡}̊{-?0Bg5]ĹM=E ,. JUjdBVJ2ei~?lޒT@oH˗/wLH_zWc'96]/Pd?pMSum>ֿ-8LO!z Z $*ͫb61<`(| 2DMl\ ע'W؂(o?lEb:g f?Y==.SN ̮%O=/E$Re98{cvs8"+F NxQ"xvYcG&SRL}. ̛NU/9upO-vϵ?aρ'rsh")Y>7xk$ۊιsJb[!?s䀁6gHVt~~jFFB@B``#{(M'^=WAKR$8")[;GԘ9"+H\++TUu6q(W5!RHM{Yȩl<''aǺLR|oIyx N@E-|{;ȉPx`X~[6)OɸIvD@䔭N#<9j'2I&Wj<3Al4B]"pHK~fPZ,=DOW"##|Z qSIhBl'J wWn;,J"+\pN[t@D`@S6 ¹wkQ '-`]&\-dӔ0-%4ՓHCxxiLϲ"w3<&Zj Ag'?DY@g0˒Pp hZ֒9 |[Hg/?+d/6ʢ>'Ju#C2% @IDATWKi@| |j8wތ9^ࡼ]Ly#3o󥭄@vԡA ug QFޑCɥ!!:ϾرpgoBnQÇ !BMt*ӑ.!+<^!y4N4" 8ulP3.؀v=uU4NFmd(%\V/@Ai ¢p BQu.r`lG|`.~*CԝM37xoL,BM*Jӷ|RkOJzSSwLKg s:}j]!vؕVsayPXVr-iX#+j̆8NթI%7Ow]U_2ꋶ.Z {22+%      F`S_(e NyD J 6߾,Vɩ7"e NO|:} ipQ Y|+@cwpSz$7BSɢK‚IAK {  1uV7n:I!͞^w]03੥4Q)үDapAYV_3eBiA3*|1|"D+K-HHH\9 Fˋel+~Xb^2jcV01Ϟbm}-[sJ}H4K յk-[Af Q3Zgz{:#zrEgcJ/=-2=ϲTAj7"L{:`i <,:} H d9s)WN6Gܚg(c%XvAIWdG&T_υKA4fѻ?j\G?Khu, Ee nHo"rēbe Zs;0 I⥚,W]O|'V/ MT+kg6'TJ)eV;bUTȧQ'[dzs=gpG/WpRx#n'zm^S:0gAZ{X?.bD?ܳ~h|EWc~uu#z"rJ&Lbה~bc>_GA ≅}ShO΢<~̩Źz'L_6ҽ1xK /* m%$$ Y`R<ޑ^B.|+ҨwhrhY8ܽƓzVX4| 9JO}=Kn*n)VN=.*`*iYKaXj(Cl Է?„ n\0:5@N[9s殅]`䦍C و)'> ||`0Pf1S/,ж /ͣ%*9ES\@USx 2F   _F)I k.y Sybw<15חp"Ym+gBVqDZJ.Aոg-˒T*GS<\(u?U7⑅1, BE$Ǫuz+~+rGgྵLA傒@ȣ*Vf%lÐHׂQ(WȝG4d aY?oZC<8?':@=w>睊'FJW)*0Z.,6mִ tPW 988_̅'.JFZFU.ܜ? ;N I4P0o.=wFSyS0,29L;Kmcax|'_;r<ᏧO[YEĴ nWl%,=妴قA,^ s˒-pIiXnrlZ-t4EM^8 o. U?+^kEHHH\yyJgK xv `2ӎw7E5 @ALG) ~KPڣ́,~vƱܵٲw1$}qٗn[RfSza4Paom΢[gAMojeO tGLYS{ѼE;6Ia#Ou!kZKA_,kƻ{9k f.;תusadT&=8ŸdZG$LM¬Bry4nJM2y?9w?n+T+aحiNih_vE$}mF]?1Y>1s%{c'*l+eBbԃǝ9fj鐿X5M9ցmmUY/wC\(w(n˒'P)ࢺy%qrA]~/co9Di)Y{^Sk5HIB@B@B*D@u /5hx r5HuGUlM !P5 4[]_Q|O},؞E%bC$tBI4Ff7ߙ{&wd70f3{wgΜIAldQ9[|f~?@p'LKP|PDi35rtLNX.7tNe\cفEZBܠ UkRtZrF;M0H&R%;ݪ@msLs)VѯN`d<),-0vp7UF6 mJ/'Eci!焉M1٘I;՗ #& W+q j7 L1xG#Leza8[W0۫ x#(u\J赅 4HBu?ſmMYLaA3,h9wW1x<A߯7S > Wm11LjLt-`gʠQ57sGC}Ԑ/G9ٜmXC}HJBFg(M50<=!Km04I' G2EBJKe,>lRZS8-X6R=4Jx=VWU]p8=~|iO|NA`Ύ'eP|"sE y:kYꏼ3xbiLEuփ}xp8W?9p^9L,sˆiUYG# 8EB [M<!kɼb Dp8]@ ]wp f2#p 0+T%G#p8@ip8G#p.|"ʇ0Ǩpuups뫜+>yE]OՌX7i.bJG n5R'chUjW7?`:3B̿o 694uF4b0K>kNo.|\F^UtT/>_ےO QotE->HL}ˀfg.:s)GdL Ue^|(JE2p*#~B`wiGCp!<#`=cSE~]oj)7cmgPp#~)WѨe/K_MD/R[(93SN4=d8ÁMZ6+z]20i={26 x.$= ՈU[)n3:9#屿[gk1ʻ#燧9iZu(tBͭ2_cD5zf'IH9<1 0l)]si<5KZeoـbOe9Д9/DZ&^E<*>ln%4G_#fRk2T 6ॻQ[4Zf&1M_Py qkEa-bcʄ䪎c?)@:~lm,%_0*~̫D)8 d뗑r4r=Z7$ĸZ?3hI/DhnDRfp|k:N5}pQ5L_cc MӑpcFG{r [Oވ; &^tU؎ZVG:# Ǝxn(=o<_^ ]4f wzQ~$`XM3RۍqO^FӑڴQ[YK4mcu7mG*ER9!xPy`6íT֔|8*PY:R6y?<N A!p +5$FE2{omM-4Z䎀PMӆs8v g5.>;M*Gjƚ il$v;gnCcj׌CQ#Qa0aH0-Y]z㙷I$ ⹏ D( wSK A,]O>t]O}3Qd%˥7vtg G*DkN‹Χ|q)Z1yGU`Ix3bTz絀t zNZ?k@=˃x6{"\Tn96D̂9O -*@JX1=/Zk=vzc|XnH }y$ǟ V2-}ǤaUaɊÈ)>XR|mH);,n\!PɈtEd,Zd3.)#^ozgcA!X訍LbY6ksob MO@G1zyyǠP&aP`G(l0 #)ÿ>Ó躓=, {;^&ᓙ#mȞ4#m$ #K1kbAIVX2<rb,8W9]AtFXj7.FKK+ S(; |pCDHS+=D]oQZkF;Mhvj!EL+-R"S6 aq̿KyJՀnG[;5VȠk7b*$L&MV/ ԴPh Clښyw/#|aG[5Q:וNbk^3]Mt;nӉMy yTtjlp; EXz1-KmܙVﵢ= Xrށa"&4.VʇS]sn*-bˎۘd ҢѠ2'Y&{jbA$?%V}xJa(TصBliOkv'LCu܏#?&>-?ߓ)<3/*t'uwDVCmu^w>틅q0QgZ$(u!,#['G"[f@#֘Us#ZfӽOn/Kц(Cv9S~lP6$-}+?\ (⠡sC\LscP{ƖLl:.9udm&cZ&V;77/=*O۠ tD;n#E֗jJZBڊyYFm˓=elȍb>d:4=%;ho'~ ӇGa@b8ƌ>}NK ųIx!G*D%6|?x5큗-y֋ ,$ P &Ga$Og ֺ"`I'msY̖h T"U棎hե/x }&C>&Ȗ!,a$Y1wX3,賾3wF+ Ƴ"z5mؔ7gVʲf$iިYM1x0a}[%Bck Z3Ѧ*Kf;*m;J۠H+n:EU;Ғ- 1!t+ϝ19W=_s_ Ҷ7!J(˙ >憠>(rŸcߏ`*2^Ov>iIiJ)f v(![TO2|NWۡ^XF5v؜Y- ZPJld*VwCHch[dkHh?^>ԖiX~:t lEE t#!)@}c͓8ɓ#DJzhW6tC1Ɵj `ڒ6tc H@? BZClĿ"v<{M΢'dAo|t|b{ذbЌǧ*kU†dza{ȫҡQy0†C0'}9.7n|&QMaNC˸,vp$?]Uavj8ʿS1W G611Bm,s8 tDEC.,@$.$TJ;"Jq]HWۨDI M#h0S+2u?I*bh 5$ :vNby)7PpJç-&/ƐIDŽ2.Zm[hBQO$ 2>u: -V+ϩ%s:\ G4 ) F)H3EU~=B^PpL=>}-"5Fl:sFܭoռˣx>$^6U ܣI٩"x!6֙Aװ6m@WdPcNg'U1qϖ9ƋpKj|>Ӿa{x&$qSl>Ď;3xr.xelIT4dTV!K2WCmA'K¶\KvR4hH8ZW:p餡4ID{k3m}-O'0*>iLQpׇB`PKޘ4[qJ"2LC?@.;KTq$ڴUsvuM.fc$RoÂN;wUE,iu<;9~8CygPzu8FZEsM%hz>bSaS@QLC9^ܱz(?{8`|h,-ŪӺ g#er͡gՋ} q,͞Rot'WʡV`6υ%cMp:īQ,:Wн)ap$9 qHpcj3,{d/X@NQdUFaƃQЖKS Z5BDB)@jz YNn2Cpʎԧܽǥހs N9fމ>XhDٻEte&$z |봥8f}aHj8A1BGM22 tI'.wž@ROUavJ_|yyFc\)ȕyxG*-p̤as03*!ъC7X55;\3]^xu" \->c_ i.„0qJ IG,bK𱤍C1O`T4 iP$&M7\EnKxҰZym!E,t=2ls)i,MD/NYFX͎!] n 쪢eD Sf,T&ubnG֞#q)dWGD>:_.h_yq VfD) nÜDw'F?s )(9ZM{ͷsܮ3M}ctq\cj8#m.ޞz>=LGSFѮx[ЯRV30jcLY­d5z6Ƥ }&GH* }$GjF֮}Ȯ@_c!@ڝJW*/T:'aW#1Y|o`\D )oתItVԥǃ`PSx &>v JD89 K]_0vIixdٶ剑 _ڪj#E#5h0/G?sM$p/ҕ4ZԫԂ/F}ްx|s?8%mS3͵+VGUUWwחn{d&^Ci5Nm?B*4tBR_ҞAHS_.jU͂VV'pqwOƄ`W=LKaB{IŬV;h*~-x]i 4}eSzcPw kwT~}W"Ix r |nvnݒeV0䤿1, {=h{m vÌN)-K𱤍!YܸtM=d0mFו#[a x. &dVO ?}G7}lg`crT^6pmP8(:kR=[1z,V W;-3\P١INxhbV[Z1?%E!tJQNf@|YxhYAԴ k ^CI>A:{cz̻{ ~ ;Z"2Թs;Z3mACڞH HuBJd֘m^B١,{l`6 e,]7H4%A _$0`Ie ;2l{+BV_X0 HTZj˃V~W{Ȅlއo vynG##6l1\nb܎:Drߺ7%ԅij*SSIaI5N:7\(1 |y `eIx7pEZcI0QXsF$6_3My_G_3M٨ os"aXWGYxE~7"@q|eB0 ̴[kpNɒBEN$iY~fKeR% (z)؎Gi Rla ? U룿 (;mo$(f-En)~xݚ [M'c0`V2/[URO2ǓK61ӲKH,o3)Kv P#`LdCJ9lO_w| "tܻf S_au7.ؚ ]2֟óB{R~ߴj5o ko{+G'X)Rb@$V)ҐBaUPTsñ)(gk՛ʵ2S#ZffjQ[-<FcGl񿟡 =cZ&V;7 @lVF2~^|(-I66`caRQ3rٽ''I4I7=NXEgN7gVOf$Y%a=Y!b)z_(a_ _j4$OScI7kr}Ći9r' Ur MT uTph=<+Z5s\%˘q 5H'Fi 4ϖ&5ld;ǒ6OR\_%~]CXMV܅(̖t޼ǫ<7(TceN;SFb܄g<=HK^f!/zóΟqI0UǓKwr/+`{0.1ivåE= #&~,)[r $Zg̽M0fDXURV֘E|W'mAȳMv߳[R"HQ __fc?k 6Dfq>e1fV`>˞nÙz&1צ񓼱 2 gFN/տ6Ђz}VME; M(7',qgm܇7I, YF½#Vܠ(oVsDp?y.P/Ghg-4\]krRע4 |Mg@ D+=A N:!.ѷ=dPhO2|NWMi75v؜YgЊ\ *C#VO ě!d^^7uJG,cD ޟiݪxy)ǚ'q ɨ S†dza{ȫ]ѧϋ |~PcBsl\&VD?%"o-E*&Zs O{pG~pST'Tmy}ܷq ]v\^OOmpSyxt}aL;W5:r6 ;{Lxy6fүK$Ux'&݀]*kd ]/ȴa蜄kvĠD?aUٞ\+_oK9l<_Dgh X@WxQjn0t1KOmInw-Kf!lix dqt;e>d2k)ފuIirǨ8&Ph^ iZ#"N=Ͷ߮S'O $Z5#y{^eOKxf<)bԽ@IO+w[pvyIL39L،1%k y^NBDyN9A'e<&zȞq"8ܶ^sv$ _ F)H3Hh9GBFJNМl:w2U錅fvߚz_ ?EB}o!yTbFR̾i҇!Y,1Ks /"{闹` -[On)}bU^V~%|F3|L!-k%Xrܑ: NtSaΕq=,o)9P3aoL#>l8ferfOC\YlʨJ3K,fKAʸTd\a3ߕŪ;6Ty6? ]~z: E TnD˹LL",D b);_c]!hA@#c~ >/qg4@IDATnz&`Ϭ~&`k5hp;y!nD0VEI8t h荘GqpHJλ-MtEdvkGf+&M;vw[ʦ-t*TE}j%~sv,͊6ѧjΚtKbv :9`B.YN-ǒ6UNINZedt?hVfR-%IBLzVb[E!q*XU-&/ $JYYU-x0Q,FzЂAW4𡻠1a]P0a  `c;/.݌h#-!HD@WOi&0D`Lc'9TCtx#\pzs﹇D(ժFZoHϴ*ZbaÇmBD(ZKk.F%Iz7oۃcU{(Yl6lN Ǥ杄:lI:E)~GZ p1 g&4+^)Q]3SU9)6m؏MX GL8@OE]3@; ƺ<| KԎdz˄J9 ˳ @=t }P8Ia!陾bZkz|\ZҪ^X/v[^;byMda2-=kb8S6Y xOǾ$.C XJa6tbU: %aS9O`,Cc(5 7by[0/FeQn|N:ƓQtǒJ-CK "Z![V찑X8S,L"sߗۭU8G"ӋU3;!Jڣ"|4:"!2aC6cNSٮ3YIWc|xl /|Gȥ>6ɣ SZ :'ޯ uؙc R*Eqp :^'gaUbӊaPƤ0: +Pߠx~Xb[30/ ֪z;݂p<:"ӈ"xYsZ~=1Gn$n@En<p.:\8[I{ sVv4ϵRO cӽt|?oKz-&MLmc EH +ƿᥝlTb)򦱶 f# mUA~eSaUwEK4,tC(Efڏne2F"0*G#S`JG=G|@BF\ux_S+X{آSZ:piUfsf9ocd'1 f*9X5bǥNS%Ƥ%oy G#NT9tO>~Ln¾xz] /%gwssy>*(?7bU]} KSƒ8u~<z7&#; hyw-=v-T:Ł/,_meEΩlJ{[3*;c2 -FaLgNQ(,fcV$}S#]-P߻7Ӳ5R>6f{'ϡFr=>Id`;'IȭB?MsBhyi w4qo[e±ՃG#U4eCq%MwoЏ1k+H)+vF4_g -^m5D6d]e1oX޸{7v<{MΈ!4>5xОL#cߩLڢЂ7y+lYu8kCVg?{#vܲN&G;0Α{X2f;oCK;J}C{jVA|.q_ )_jUq:^V2v:gcbԽ@IO+Y(m)-q0 `hN@_v#ab۫d)ii1z ̍N|b#voC ePo;(յ\q696BjrKT|DמSb^?OU[L_!qoGQYpn`$- hK?#߭Z[D+ sF@dl?h"Ƅ8_KXE e-K Crr໓z HС^L\\ߜ 9x(z)OcG#pY,9V 'mZd ~PXWuj(y5ٯੁZ,9xJUdiC3ZZڵ#tg@)LWKFx گVsiȿ& p{|-ФrE|g+` L;, 5::] TAu}["׵ F`d^$RL#!6Ml ǔhɨMI[d§/Sv7ݮiiei:ۗL2:iC_КCSqMy*D,3(oous,sG#t$s];J}=LCwy}LD"L'|M$$ )Hq<MT ]~z:0DG⣰qW揂m>Uf[)8yR(hkk$ _/?hԦ @_ƮE_bdyyС0c*±zh$<Ð8CCjcw|o>q?t: i Skwwک .%.(u?7hޜ$:@jcG#X3i @UI adF 2Y9ٸv8f.)b)/YI" ]Iv< JLi11 /璣}rXlI.ʁxp!߽2d¥aвހTlJ PbThjlnemvŘRY}496g8G#pY oq>atÞ1+4{JTbTFUBq^*g1 ^ PƗ%h!ޛW(MUH?vB4zw#w:,Y1^>.P2KYW{k7!ʫ ̭E j>_Bp8pRxnB@{,s_qp72*#p8+lG#p8G@ eLp8G#0pG#p8nsz\о.h.ރ8c\W#?G#\mp+w@;Lj:Mi6b]5k?mgbX/9᷁Mi|/8l$Ɯ{+  ߄{ƄUMnYVtDڨu5aJIO=Gz-Q都\oK>ŏcAs8G#pY"/|pWM8npt8ECn¸T!jvaP GkXEZ*PKNm`'5jY:_x`.ߏ'0v/`/a\+bemx#p8N;ƭ, ky}Rƾe2CUjS˯֢[#hTG~G r.u(5.-HVvnpRNMRG#pz:\8 VʹtIi"j:ouSm鞵(:C]3_~=&ґgӢ0O.S'O ktYy5Vb$NH7Jxz^YuF;,A^p8@FI7S 2?gyA |%NK% MckkU+'N^%UQR$UW!z#GPbdq^G^p8@@ xM;wMۜn]^CފDq,W&?l^PU x0Q,Fzva,ߦ)uyVaq@l,rťCk!ZW7/p8GF ;2&wT:iɌ͢=m{{,ǀQܢ>ߦͩ9Y6A3Xa~G8cAg&4+LDյ8=9Uu8Ua-pr8G#p9!jyi4d.!_eVsG`Q Axx&F#3l"WW'3W|"jTm:7-7=wm{%3CQ?-V5 Zñx#p8 ɜGsh- J5aZc@RAʜŭV{c_DŽq ,P&y{GUߙII'$!!@BW\"(Juſl), * H % ɤ$3;7S2$8'ɽr?MMGfAo@oȭfWGV!E5rm l2blsܮ/SɠAF 7"͗'YG#\pr.gnHРBFLȩN~ Dci ra#m16GjkP(NՍHI$4qJv(L2 C$F;`/1^0q i3VFϻ&rYghVdKW1\DW=[5p8@w06#MʙY;QzҹtdL\*cGdsH:gG)ozd CʰUt*hQT5|A?oOՕa_i_:_HSMb][81:𬤫/F>&dp:hmX07zD\`%S=3o tg*٫vRc!l'٠{Pb&J6{ևG#8璺rz)S6^lL95MR9;ZKetIfّ%v]JBJ\iZ줗A_>DL4;Wt`|}Z_}Q*$Ֆ3{2kCS;,vV32G#p/{sL{*(CAaC.>e;~CguTBO̻E:#p8>T?A^'ZFe-OG#؇'ĥ8v#0HRX}q|!ݍqAG#\eprzP>ˋ\'}/,-Jo#p8WlջtڼӦeΥ2v^Ltޗ#[oZ_ʛ9#><£0wxL~8FEJW&ގE`tj :*_x~0<.h7^Vs%DZgO%"G8|8<p8W56hŖ>ÎHL^ʛ-Yi[N;*-vX:?)i&Zk婓oLj 2Cz(/pcN=;ܽg`ՙr-butm8bNVzzum>*G#p6v;Pi# 4'X1!bp4PStyqnqE쬦!A#bc0S`Wԙ 1!Ia 31#+Ej}G#p8v!i0I[eJn,Iy1'wbK\ɑ>;2%]`Ejr*J^ep|OZ8W9G#g89BiT/M{gc[ƚgScE r-.64;a❸9E;O#atBܠ#Klفd,1wË[j/@(Uç~}db2d,YtTM}U"Ǭf~Qc}'ۯaGy\C~]9G#:BLC ^{MC4=_5ͳK\BJ qrx%6˩d{V-곤+x8[4>I٘U ዥPq d &VJv,˚xm ^9e\\Ž>3-cօm^{0l`B<2*AUUv\$WSz#3 qG ϡL40H^zYn-K_p8%DS'*<=\tnT Ebڬڃa yvbr5fZ!1N$!$Lޤ*H*j$Sn-o-zR±pfc𸛐#M]:5^hӵrG#ZWb!msYkm*F:58{ũ2\ӊ7T˳Lk:o]]%mY]mDGF0!CoN$7L?jھ Gkq-yG#p 89^\'¬5 |hzќ^sC(pM\\PzDG} ^([ފ`^롤wUmւ X@r9G#p"5 IMfY(2[HCho+S ,x||$"YjNckAT ͽѴ_[ K?mvsE^u_>"Uٰ֒xJk9Vat"3p8 ɩsp[spn+I>DL4;;_>UШ+" R2d`hYm9i!hVgzB@kQlR購Q$vO -grz90mr8kNN;S{*(CAa(:Z7`p긄Z_c>O @ r9t3_f/ar:y7f!Y*l{ܷZz1;@%Df(o8 {sdХIq_x8mKE:-Ԛj|rd3lM3GY H~jqMfW"1hGYeOJgi㑤B<~H-( @d8ֶ" _n؏|ep814GD#x$zB]`8Htg)+;~'ܬ.)CyLDvwԴ|;8, 8pl3iC͊]E 8@,x`2<5cs2QmM%ֿߝ®&p8"iHbog UŎ[E f"٫?_-Yh'~uf!NVJnk6+q׍"nkksXsA[1՜RaZcpZ[[WNKȗToAA[d[v);`*9XLG{'ftLf\fTT.;#LQ6> Gl [%S_Y]4Ua kv.UxG# ɩ ]OMa!;T*(n4+~m!+c~o6]u\O"n7eт=XTqBV?"xNU0/6SD: BHr!/e]ar WZծc~]-r<æ2sC y^)0Vw1M#'⥥08j#\prMӔ6lBRUd,{ui.uo͚ui4jsdZ#R5y)ٱfҔTД0Tƽp;.̨iT}~Gh#DQ2&PJʨ Q5Rw6gkLtSG#p-!iwƬfZ:s'K VkjK =fL>uw ˞s n,$3nnz_̛}ı8O[Om1 )ry &Ĭ,;_)P`[1*5^nl]j"Ί?#HƒEQ\K sf(Wϩ!y{F F*>c#1$"CMfwYBAk;c\s܊ANl͕E̕Kb#- !k(kn9G97,kԂQZJRihۃDt=CPЈjc <<1^(HkI; )>⇾ɚ/p8+CkrT#P JJBssAQ¬`:עdB= ` P`GWI+Cc]1͝sfc#UBQ01";F$ƬTR*vj:a7LÄ0b*-(S[ %9ڵJ ,֦GxPvK{ҵSՓ@,# IﭥlS״p=`YIrܲCB&ytvB(*;Q=ut :tzOyYS&o`+jmN!ںXy6([²h/YŬW~PUJ*:ez>Nldxp8ȹֺf kRLHCDWM@pz:2Z`AlHxOk= Ttev X hCPi0aŻ[pn [Jw5I^p82tQ9ՒAzdR:A@Ô/2gF&1Iy3(GH$Y'K+Pp׏i"pr7ނPSV$SƒgLMkdo[b5#j4a釴iwa#8z] TFLKmi!?rb ij<8zü?)yDC9 +jˢqbRʪ7 ;> !-3A^A5>>U \$׆].,=GsخĿ?<򜜚2XHxp8@/NQ*Xqp/"L43qΈ!jPF I"6ģt-Iͣ%bAԑЗnr8~`.mb0̖{uт2v>p8@E[Npo-1-ӷFޙ7 3󏢑D#b=dT+j4|δ#+'Qt0AG2C-GȠ} c`TҞVWH̶9s>Ƅ\ĉ('gB: !UѾcF/1xYY[;mm)>JF:ic<@EӘ"Z"]QQ ؖs?4#n=RԋMov7 ܎'E|(X1ys2},snH F7wW%g%ңy=1gO[x(^~~G'̙c_(w>6ΕMfQn]?w"+?X(z8s Px"&)w<86Aes99rjG/rx3T4Tֈ5UFѢf5*k}/o݀;zx}ٮ&ǐGQre v)ݼ#5C-hy{ *$ $OzuW*Ɖl Tc⠯ڇ%5|Lߵ!.(/x"Zf@*ڔW C$]1 E1 g}:=sC_xXn 񆗏 "9>@nS1& Bc0s:,V;l4nO[} YeJm7ԡDp$Ad2#cYKicw?v//`ѩk8^ވHC0bB7ɞb Kɜv{xɾғ15-QۧP(Sۺ{{,oۍidEzRz6HXx($x ZRSԶGlr vwdٗEa!_{3o~Ʒ,.@=iKk¤lGlV2; -K35t)CqXJ<n9~?#Uٸ?wZ7|:c:f17b^x<3 /={&猤ؼ/E ]Azzg_M [;Q4^ɏ-sK6ȷ6y@]@IDAT~s,36ݰBК/LwoG7Q:T9·&21kIǦ"/zhQc&='8yD|(DX;qs];m5߿YvϮ?xh m-.{|>m(=6;wd *f+usD _$ܚrTN{gOnv3kڢr=^#p 5~륃gnFL;PPF* a̟҄LLG.QP_FRrq Ah[sG+.4thqQK/B=a3G#6*?D"Sif϶Dar_ATT1[<5SϩPާ3 tjvIsn$j)L AU(ɨYzf2UxU'식 x(,|G6J"㔅IV+ij+QIaB ]:Фf9^-eXr&h4v&:.?WCl>绂*cpc0{d|Yt_z7 /}NY)OlTDZKj1yG-ޛ9G p˩@Y/vRUZXu~SL${bA)ȌBkQl(scL±>g4(۰kZ|UO{ϙxfXMd C'MSsh1 ڋF. Dŗ 4'p(&օ'C^K>!=me&juqPmF=G٨N>[n~/3f$ioM{3փ5h_0뀕v%cptd/$夣tYcOh:W`_Si[4ŤV l#c0kHE1 > '3B/gzq/}G0:Mȭ*)M%|_ s*l:a6b?ǟ_`%^ݡR@᫄+o _8y!ùk&8Tg d[QحTwA~p')>:%w(PAfC!sOč#uHϥ/6R\Zta af[bJ6%H+m!곜l4`3y񿱁^_qM9](Hm['SX]MH5ݩT(㝐E)>hQ&YթĦ 5n%axѠmD0&}thb~&)c_&?S*y>qڈu\DV|,clދ½ 7.Ҋ 3Zy;i5̠h`g/-}E~av~'Vaq~a]lJRTϻBe: ~CR7d- o&dKx˨A4-~>rL0V[tXR[ 有X=3 Jv"kgV5y)#4nje+-iKD@\2nEnLغz 7BZR8>y 1ׂ,ڨYlm:ˬL]G(Ϊ{(\;x=rߝ-R _6PӽKϯMnhn)Cm}S+⥹شF2?ԳGeB Hvs}MO# 1jNQhvC(66Cvc^;T?;{@{dT5!10J{8j ?v'@}(zOm52ㅙjE!~p-m =Pʶ{EEHN}4 07r0>c#1$"tRTq7cN IC4X>堞.5Mk{0RqRca1lVf:ub|K6]DaؓB>>L98IL\ z7/>Ill F?d|k.1a9M:g+[ Z:{k=VZ}/ّ_a(:IӁU8[xQ{Ѵ2V7TwGWGjT"u FFl};S0PE;J[!?Y0-F.}-!tCH@Z|5CzǦ -JY|"؏F - Gx@D0rJȌpf/-* `,XTNzMU]qPC1V>M2j[!~$hz,᦭ho4B@tݔ$YNm}TQ>ݎ&ڄ'yL>͇w' 8 ~"i)13cɫ[8l4WOnīg؏lZ56&\Wciiѥ=?v *←8"|qh?I ,HJk ܞDz؟nEDxB w(b(}d \*镖?*5%#BOxw εx24E$Lc$ҏT v1xhP"|:hIHNfP!y),L[҅+8vCi*tup#, fcpOH֋1""l%Ig밚^FyboC >݊翜Rjc8*lX]]6ZZ|nZ%#G#8@*KEA89'7MLJ Q UAnAp}$59H}z)8YH+-%@6%7[>[aF3mZ"Y[ŪOx{oѷ`Tͅ*Q} ~o2Fҭ:ihwirӸJIiPrvG#pzwnN`CxKחcyfGF&]XC e!\zըհxez"&9)(Ѡdcg2ZzP1+cL9? uaWMM?\۪{Ç с3~g?ۉsM4Sg`3alF8ijñ. ˶\G#huF [ B(i!FQRKNĨ16K?` Rc4XZ#٣LaqB0{00(!R`7YVwػaEXT$Pz%1צ8`2կWF3>x^lUUS'Y`<-$iWkͰ@JN|mK|%[}/a&V[-fl]ס?qKrA#'H%oD̏x#cPy?mXENr8~PfmBi N Q&W͍PG'b F8-\oF6uz2w#5{$}Q^dLG<c0pTb۪-(RGPktj*IHIr܊*Y`'Hyl&?ӊ2hMˤq}v4u8}0f+/e݋1wEF};tF3B(^L~C3j2[C: ]&jyiAs=H"9>.g¥c,ֿ)p7LvpBx=R=b9c$\G#pi7ᖌbD Gwiz JB B>c&gFt֨0:Xըv^LQ[״D9\VPpeOXXV@=l=N5CbsK NSBSJ8qG/wmEÈw\-ix,uEuVH7mgAC[-ᮈD ^Kr˱qYtYXR֒0Z%U75xELg8G&ڄ_((=c'{NzL2!>;ڲ XFԜ,BՕK k c_hI!+n e#Xo)4ۘXc/1Ž+,V32Dr0UsGYmqA-grjYG#`ڇӵ)Ÿ;UE)d>o?ml&Ɠ]\vGh?v~~n Ŧ^wk8Ʌȹ&ő7qIsc[qLt)bdNQ%j|rd3l<.A䭝(xD5czZ bRͤ{Ԧ$p8*BB| B@kp}4Fu:hK/vhhO,ڳߤhƻq,V'h`lȡB3JL%[z5 ([DN%A _%R޷ti|yp8q̮h53ګu5M)?% Z)ȼіX_,kĴINcp2o[ :Q㥟?CX#f16H3լ@W"1㸄j{'tu*֕Ͻu@U9yZPX4BL@sGEϟڋ{nDGHɿQ7i}nee;N< TiH'lԌIKJ® vĴ{UG#`DS#;*-vX:?,Z0^-~]gtbPLȌKZ8S|_6 ] ddA| wcn]-)~'|r]*зA#JF2Sʼnx0o$fq\fXUuqPenGutm8bQ޺4.)@ f}X̪wIRTM'u袎p8 NN`%nӗP|P%4XN1Rz1®MF93^fdʡc~n_~|/]rF[9#' r\,.ȯFjy*ϩDB()Ohm:2%j37SAKq}$\iV+ GΏCu3>CXZxJ 5ypZMO h.ZmGOKKa Bqϝx!G#WҢg 1b;EbScWӇc$I GY+ Β+'(mӊrH Lzy12mHg;#Ϥ,W1sΟ7"XZ"a83$$7X\skjs?!xe=x()f@P_u{q_+~./zf&uq0|S7R(O jm ՝QDIkxk!y1G#W{={b0Ecu8/p̺:v),J˵P ^$M ;^*/ r#~_a'Tf{Gۀ1q1U7hk+GG%A`A}6Vo7)R4d‘ӌy[.9j]#ƣ. =?}ݩm27i6\\ e }ԊԆ;! rnϏIHuͨi?-"ku,np~[!:?+AEWb"iTZk`ESG੣ LUQ~/4kʰڞV][N!Z. ;gl~KC1k핟aq!TUG)>ʻL4ϥX2.@Zgm @VH$eiKr} UCs9 0((wS> 2xppFa SAW|:=SG„n MaX<M v`S#۪$I )ȉQĸ8kˆTFrEX`Ok:[&ٺ}kP)'sG#䴿\ ~]; U~tu"1e"U{_> #¼| $VtQ9 Ƞo=z2:dd!5QpeTBm $O@|9{62)%7#$\J%+kJ̞]]l7Hn ծG}H0u~H!'zOg;..kHӗ\ԫ<w-ξ\i%ZeG@?Z kKq-6!'~e3]jYRp8k  6_E$Lv4 lz)IKhkPRo'p)?* LQM3OHS9e[ۓR„>3>飤>3b*t"4'a$ Dě}0:L׷UAM99Npݰ@b->U85'f+L}QP,8O j #,݅eLbK7Q:v^Dҧ0B  ݹfʖ2k+X(k<3-ݞ9U ps(Hng:sNJW}(MX$J0`kxG#p8}GNTGn.)g$ç 2NLBYd´[Hn2]dx$G10hV?1P1?M$Q +)ga W"¢̼v\wmuzZ!k!AEVdM;6 ؖUSzygc܀x ?JDJc=¦Wh[ջɲ^2g6R) 6on-0&:S0I%h)Fx+V/zr3?ӧ4(JA8o$oADl(\j}у>KW2FNƋJ4ވG_(?G~ڰ֝p5Dh:PG!uB1,;tjT>ɥm( JFxJ&.7LeՅX5蠌ARrv j92R:]z?$K8B{Ꚕ7-PZٷsh˕鳳(7NhDiKYZK9Gpr  Ӝ^OI/بWҀz,Ꟊ2N wdlҢ MM!"RXƶ-(ߍ"hIɱd+jpܷsC26 gr9]㎶ot}%GmJ$?GkK@O.lIbw ߔvV+#pzBӞׯI{!M OOW+QTK zaj٤ %db MZ4jow2^Рȩ[2 %<|{58? *y+ ;H})ЀO? Eo(]' (׸";J9]y%Xqjg9}d8b| MlUvYPЕҙxso$U8~sO3UOTuu 2zkrWQ/7G9Z~#\89KI${Gd!֘͞ RHpLR"]C\=Wt4EĜ^s‚v("X\u(;.3V]v?T]U흻ovhn=K x>4ٚ%MUt?p8;.NPi)J l:$s0!%ЬԌAvdrLEF>0vV7l_ڒq1@L? '9}y}t\o""e(-mFYB{z3oڎKk!8L}xf$YϞGfdjs1l95b׻qwC)ts&OBjmԝͱ75|G#NN{ZoCmP˱HY ef]|;X~ XB0N$W6 )-LԖb_a'qUf{Gs E sBL ;Zj#oQIP*XtPǺ 5`3(Aj* ykirN1o\$h:Cb4؉׿.컦 ν5%slʋ=8I> o̅l&&2\%':dOt\$LÍuIˆZO;FvhYIIjuvtg4k~*k ۟]MlJZs MCbkC# xxbPpQaH -M&W)G#؋'"G9ao{a p2Qeօ aBk@^gi{'38r!".$`,\H,i[=4-KӐGN4j]CJ6TUPGƂE>NkKjӞ$"rSxDg!2HȬD"n-by!җ>z`b ^ 0pPWQ?|BqȵhO̒D褕G4YnKY%˰Dx󲍠5Lص~z?N!_;mQ>e+^Y+o N#^j-2iX-\:G#FW]_5x7 _5͋ĩ~i¿k3,ȁ"1ݳj)V%]d\4>I٘U)6A$>wR(I%A# EDPWwmu6!ȂtB !ZˤO&ʴL2{s=޼9sU[*t|c0L<<(]/Fé ,(>Y_ ?od_=҂R0w9۠94 a .8zH rP'~F|Jm’]PnEx操51An +7`GNŝaY|Yz#2bVO4e8SQ;\`pP㉄ O ھ}I)=,g<]hO;\kv;Mom b }.#tJnlš&[ Ҏګ?'(>yT pdDឧ_qr7 4:}T'l "Xݾ<{!g7ãGU!iCTUonTpJ14͞gh P~ƶ9$7ގpa~JT֋gRRZN&g#Gk>EI+Z- B(+SxI[@', qHJ0YQU%Ӫb <=+{l Sb׊FPh)"[H9xp8+rj^Tc'GuE5Jʐjr-\Zoޜ8IƖl)À҆r8##wKG}gj+HAXN  Dw4-58ѢCw0͠Ff 4K!~l2.#}1f^tqIAӡLDܨXAne8ePPL !T\(>P{?~@a 95WNK >uNUAYYMFzWSgpw+(]RQ|sB1]ўc?/s:ԹfBܫN1ZuawQ,iHgLCO e*{.Zvk ub mH_LrѶk WoccFz V'[p땭4{mH s҅ħDq#\p|h vKA8O!v E#A yaF,̠){!l DYH\uyX99!A!6+AEWZ[/(ufyT'"eXr/X=MRLY 5' &kaKM1ԗXݭoCfThsYneJ} ^S/Pr|xG#p# qE@_mZ+Oi)!y Ԏvڼ-#rŚaɒ*>VZۚLFs)?̀38_,@IDAT@ ױ欑ɍ?x|~<8nA/}JjleĠ~vze^|U' %a2T$WN/VcC,_oj $z4aĬ<7}d4PqksMIH s‰pr̽&5w1|X 5j<2oLVY,<$U!X mI}Fb6[dRHVCW ^Hn鸓)XB*W8Kg&yҒ5]pQJHD'tM˺oyu5I07>1;"Eax25|Fj_0ZsvTvOe3`? Qhp `q)Æ'G# # c/">>pPKu\>4(#+b0fD: ',~ >C3̬GԾ2eϏ0SA"\I=//Z?)$f>^'S+n:W;}:ji+#1<|s=YS{)[3-ލLRtr,-kOs#a1PI+Ns~߱>y&ʬwR'G#D̵K+Yrm$M [94컭39S,`lWf eK!%^Q*$i +3ֶۏ`Uu:?o i7Rc1}GZt'){Y1`R݌=LIN; Сã08;A[o#z9nJ'Q1Ns hZJ'FvRѮP @ Nb,>džv P ŀ?RNY6cJj ΔcGe6}B1~r/a`ҪrOxagaꔍK g'KN!ύj#Ţm K4m4f䱫KqwvCԗizǔx7M<Ē\Vy9_`kxSX.y(zo~I}x4@G+MS#~}1>LfY)r|^9t<Ш/;P[>!c9?{pg0r3w p8%EWys Ga}.a49(͏윭H Atg8#@ m$-v}kJ'R}+`e!ևbj;_`-ow3FC)4( 1[Tt, x s˫ (R7~5Lw킺;Uf!O>}?ε2(ܒob+ªw2%N(]_\C^f|XWcCe d=4 m(%%b^9s#j"K8GO=r7ң}Qr;",x G#` gS1[]&|)}ؿsG(rv>˨JPfϐ'7ˀӸs8[T43'CG oR5bd؇ҡ?פ@po r?UX j n΢ԳP?!WXg7 -PKF0Y~:2M2: cW81;Km(gf)qR 7UV\YC^p unX9 G#pi%)D@W+wÒI9ec ]ȱؘ5mq?njE:F0SWVi}ђjl+'ض{pDWSVcbqm]Zճė:g2(@^L9G#Q鴚W\nV>g3($(!//VoO{Uumnd~za5yKR:2. $9{#l0A- l'G#p.\9^" `NzfPKC!LkmG]T^>;k Up Z$Ě74Uv>66m,0 3zq 蔛B ) V'@vER VTri*G#G\x˂N~*BXZ-"?|5ff?RWgМDv4^*S5b^CU>KFFX<_Z@9h FBboE-L^p0⤕S! tЈFīBޒ4T|3Y_Kk#׽lLj]5/D8/}o6rO"!3z܃qbc_;2,blg!~2_WFy 5~4_I|{_{pUr }x:H>7Bt2(J'Kp/RL+i%>)fisJltEr=IeMĹ;>=IM^kVOX8ж]521#$PRRy>z'V:u%XG‚C1TUH@\^1?t 910]O)xf@ưOVwRlhסVtgVMU!2J 7(V/"$^N8wE5hAIBcw@/nP6p茸iؿ68\TO S42d/; zRKGgW=fCH^t"k**©.e"G( R H 'N:I_0Kr^Qc~H]BRa5WdY݅f~cFݣ Eǒ.o1!ݸ; ~DluǤ=wׄuDAe2cN~2".b5$Ϣ1ڿc.%#/6yݸԗ)rERcp=đڀ3E{Cciks !L v&Udav~i)h߁?&.R̢Gimf,4?dƻx)%>IqPTH+EKw:jG۲ ݈]joVCY_ -@ΑpDI6?da)Y/$'x VyXhw7hLGi1܇\jt-E8[3C0_n#(mf:E"h+" ?Y.q(U*4؃1/rLߍ97, QC/pk& ʱRx?7mxR{Gۃ>0{9W§{~/ +Ѵ렩7lŸɯ( =&n!}'1ŦgxcpIG|YΓ+&a Xs0J DC*0D"0)muȭ)Aɡ@jY3 %&bFp I) 7K\h:\|ϛ3h ނ6lhG)ʹ-pl^"JN&ᡈ T>&OZ'+@[2(nއvLi26:!8!=46zU#8㴵~33Ŕb>畡IcOo\mhnђ,]6=輥>M&N+y0 o \U.ԢF?aad "TSXJĉn4*5WFâw[ޕXwclNIEr&5xexN'^3l!U`QMb8tv=)&V< >ia<ג=yP*:c4uȴ(ccg ![YT,1)?)')cQU#wm_݁~ hU^/7쭢߮" %`zËG?ڢ<_duc 펫(CᓆP>1ϲmS0 QdW>Cd&+ 4ޢd W~{V EWބQQyv G<5Xq1 d$wW ȜexC)))Q$ޓٱ{LqWN 'p.=ScH°0t @ʓ@ZVt(=G40CbsI t_^:Wq4MH@?/"n,pi3ɭexuĺb) 1[Q-ڜi-BBM%ԯ~; Z?q),uMcǧSda%^4* uOp Kͮ# |HxSAl933<LX:!*\@ i!=a'UYI5SL-$-#).k<1w9+!;*\ -D@/ <{gpz i!!Tl?7q@S5i%:SJ:Jrاb 1VTRJn>D7Y*EYѠ~lwHq|fjj+2)pn? vkv3پj fӵd,u)OF[c1Y|!5ј*bP,+>z{ .9E&̍P݇˳0=u ED]Z!S2[ ^@+}j{+YM)@Gv6 orskbmne!H "Ŏ0UR恖$d**eĬB|-ȃcܚ-\BPB)[G6I3:(H 8A 2J>MfM e4\r+S!U4K,':9iH+\8)b=%10Ŵ ^UZcN7lpEZ %eIҌi̸~ͪ>ǙJ2Ƞ-mwc\EG l_WeR{gwKi|슅y݈CBO2cȾ[ ;Di1̎h XfEײK3l+MC@X~.Ʃ蓈[Gch  G܂X3@c'feBR.(v2 Axyy`ü N ?I}<⩆7G 50 4xn۵˰١05\X_I R-dNu5-KR:N"azĊ1_bx+\y5,歔hWfϝ-=k.-KϛVA3Mg . qsGhmUd"G:p&IP'3t*-( uNVI}!:2.4Ѷ3Dzw8v"ŏf]b|rG'=-׼@MeP4i!*=,2WI^1јKX#NuC#-Kuy*FyV2 2ۭnGlRŀ+ҳ]p-`$_cΉ{a ڒw{]LS^1MOAx+*b-- xXN:]#XU)з(7Rc1Xe>>ðTD b%Z$#$>?Zv[v$]TXXrA-zmQO߂ouKN@Fr ~7%36G_펂jϋ˓N<|2b/7i| b媭iֺ,qH ^L_L_ fG7cĈL=E}{XnNpqH}v7ebI?̉_CS}%|&Fށ 8)|I֬5`܄pGwCߛA Z\8=oYRㄙ  DܠXZ$a^9aHA);ze bO8)3o|' j<4gTe8U l[ nFC+`M(UToPTȯK[o8z-ј7m0<ɢ\r0(rIGx="b1goJrSec֛*|k DrmiZ[~!Y[>$y^~jb܅ё! ģ ^]_8g; ?n:k6 5ʩl ~[*%T}Lޑ"f(Y&k[D/D Y6NTzPvA'OZ-0Z[(R5~Lgx*/7~0W>8Br5pM5SCDDGY#]'H$ 8a 3~1ISIYN]4bFSpujqp0K}#S1o>Lfr.H%2ME9>}žU :wS?tE;MLAµj{J0|hWgN^sKu%SWL=T@gGjv#ݘ8cO7?ů4Oi1/ s OoO5Z킅Ǩ=t-P_{٦)@fR$! lL#5VunT'f}a[A}1>i۲u]f]1b~L#IR *q~>{!Gݚ,#1 -Ucz20QA렀|}`JKQ!\oor||TRuǭ̕ <Gw5r()L:1t:Ҟx8+@Wwޕز:awǸH[ë>gy(lS6A1w&G@ZP *mk [Z\JR%!&5!Ԏ'5`-AQbH)J|*l%¯ PLdS yq?Hko-/f 0!C:zjZ`ӏ?< &ٚƮ UDdC<]-+]ZM>&R"r}ujv1{e]B#pU+yEjўb]eTS5ř]:kļHgwxRhq*jo[G6]x瘾BÉx?KmKʩ9Wbn I^rЙ( ƀ)c<?[](!c%Z1kRI,"%Z *}&) ?"2"i绎FN_Kqu6 S5yg*1`pS??)lէpLPLQAn1I9/0 ̙&=(,[>[p.)dԒvCmeWg\ԳPs [Igk <ݨEk:6s29B%Dq,,XL*VzNRf~eo;Mom W~ G7ӶW@AL tj0"mq?njE:h+AJz%M+00)ZG+9 4݄s5Eqf5|`JqRN)ָRJ#8 '[8$(ɱLQ4I/ˁI!6e6e*= x. W#9jeyI kؾA@&u=bS_!ʣjv}>9=?87+Z0UǙկf>͊x@+6@EgEqxi;yX Hu[:83Ѝ ku (󰁤)7)mVE9 @`Ĥ gJ{,)} l*h >'Ŵ LS:}MYZn!s+ۧC:h S4;$}cǢt/)h1^{9-X0Fߏ7ܘۆCaL*-LO1#NƲ:MCc%>)j^Ў;Wh1ڽg  wt)ޫKߛSH{筩B'cq{|;oukǏ [0"z~ؿa䮞781䯝clqu%ˍ[-Wӵ'ҿx N+)~SLO͙+9ɑ~5FXRUYU)E5jP^USR&nÚOAȘuL- ,Wx,_^}VddMf27|X>L"Mrcxc|AMt.qjox: HXu/JB^Töc[~AR5ecFýv( v²hVW+oSuSE)ֲITHQC,j{!ӝݩ5@vBH'<.՞eX7x(OfW O!Xw1c"JmZӃ]kroAIBTw>" i1c{x_.Ϗ+RP:OZR(sSʰ1&O95DQLAG(6.t"^HW*:-+!EvPa5W^1y$;+IuVwcﱡEAԆMW[!_C,LR>w4 ZVNN[_:z7!=1cȋG|oс0ޯ /@N OI')cD5!T}ݸ; aH emA {Pbc͂sCpwdz8-YҜ݇O}C! XH-4Lm%b}1:4ns !L 'Xۛȅ$ i&wO Q1k9XSԲR-^槦>zhr0Nھ٘;uRy& |ΜGsOcoރBՉ=t,}n ѣEKkprGY/6?=pZ/OyXhñq9FEzƕ)x[>mxrZBhSx7"+ ɡ0)1Q,agDtZT2<1̘u%d C l ׬BzW8 k'qY6X˼ Es˩xKnߥ pl{ZKGb,%︡ H e #,4 v$E= ̆aJGqEB<)ֱzZ0t@w}5l@I178vᡈ T |vu&Q#uHxL@\ۀ%N{25mܠm-t_8 X dh~ %_>M7[;eghr IHgoSzy\u?G0K=_Ok\s)8G$,CZ46œ@cniiͮa@r4顩٪6mC3m]NZ']6=aNw$[d7`_fǮ{#C8@s=| O78U] fyJ锄Q^T+sǐ<!x,sOӉcŴ1w!X0L#T8ѰsaӍ2<'Y* [{)X#fg8uǡ1L1)q8r$$]{\KR6CP1YDAn}1ccy;E%NlR.w+((9!јKq a Z̫-7Q=j$G LE0Tढ़m;-n|S/W8zߞ+[u[]ү`E9v0X:X*5c*g'bJ3߽}}Č۱|~зh)k/⭯c^DMSNWJ.Qx, VN U,G45ކC'~ Hc]J:`u}H|G& hjĖB+a->h/՘(}-Ƶ-hf d9}njͳδBJ=j[B`ы69٬R,chY0~]T~jz !E\##\RKYkaܶ_m2:.ò뇩Z\VMZ8j!h)]:W;}+'BQ!K(\AVȍ Ԉ?wyT3mEmnVE/(bpSKق.|?JB2Y 2XTw1^X =E"UNꜢbc-}6@'T=,>[G{Ͼ-ɜB 8{ ci_/`wLH Z2/ 6ĕjqꐹr_KߚwdzRNi\!}qQWVjRYQG:a\JDŔG@o֊]]z4eKh?!r NJ0.̄`;NtFCɧӇc`/:ߟ8ǃF#0VT DÄV ԁ@hQd)I^RV-ԄtE{<~7gIhd"mdΐ,@1:!)QdR,2F[E6XѣA|S4#+Yۚ Mx|v?/I)*w7X!c$6B3efPY v SxpX)$W85p>J\1/L ACxmud6xyxun? %L9MʘtsaS2%?^aF bJvi\T Yh Hb | o,mӔcj*7HQJ,\ %conlf.qqP7eiNYȦOM7ë4 ӋR*PV%e{xC݇Vt_d|o$܃_ Q2JrK"X9&.تqaƦ3xfF,!%Y钊S#tNG43QMnD~ bb9XewCOePc堩+D,);E mw ď*1g`ko)!k H܉CR9TEF!,!p! )6/54طz3ӌ=>5xчqfƖ:+sz⑇Ƣ=䯧ʋ9H\nuPMǾ!N ^&/wWSU!'M4˭5;.̔5i+[]F_KU&WJgɼƮ{#..aao盍q̨bYwTpR0OΊWLYa.ׇ*ꖸu,ƅ lhbH,HܦHl^XPG?n ~+Ȧ ͒L` 6fu7.9%.4t7iʳ??+KVSgaKulN R L@40=DF M{<7{bآN*+4zw&zr:d,J#]V_cV+;Kf,+W" N\']cb˪b}=y`K͓gD8.^DҎ2oyy.o,׺&Y %~ HS;WYؒTpB^\'+ꕛ,oюwFfg "jM&T^ C9xBBFv`,n@B@+t g@EH@<6bJ)XZ*cL9OS♊LoSktS\&?SX_= ś#Ƅq*q˭Tkvtʤ{1"-{%-T"MϽe8_޽[kdd^xёPP\;hتn#;VR%M0<ؖ>x̖면(୅ oEҠ#ǼpUuYͨ pE4>HNQl" 4N)KNni Y\jR!y}:gC|C:,L 8K6C9h2&C0`k\^ad ck:Yo)b߲ >(Nkd')jn')Q,?HiyB}V|dԡ6H7jk6H%rj fmFu+13FOtz"kupU I9O:c{5 Dxxz|\*)bϳ'@v _P5z%1eL '5ܱ.,V,3M@N@̏07'B\ZqQM>xS[u%Y)sa8cQVfʗu ; W>2SQu:-:mg0cfec\F$b\Ǻ]螉` ڥ[ÿ.Ijֹn8]*,{+{Q-qkPO-~q<1DZ\o|VtR U"QYzXhz 蓋ǒWedL_8 G)VC2)£ϕt% HMBW RV\+ UV^Z*v7bM 'V>Tӕ`Mv'ݰ{ Z uz ݉nwqG Ct6[-:YV$T?zO} N r1_-(SV^qK`NJS}R9W ;( vІ` *:1~(-ZhkJ7^׃vvݗTk7^ۼ 7~sToĸ͇, OB"rj-ǍA<0KSѴ ‚"MiU޹d k\8{VҀ]X[}@d _TISx+YskXùW:cU$ݺi nbҚ=oHrJ8 I9uvҷs0gbb;|B0A6(h7>.xz H8aAܞ- ո`:LA]0MMDPD%Jm X dDeZnԥ+G/Ǧ=+'=OIifquX*S c s$];ȊxkZIHHHHH^_1<s/f#{JtS8U<#1ə.BDF~ǥ\X--[THk-dGYqm&IR       +HS+tXFsVJ7є=fbLOCW◶…65Q wXSiZj0Ʌv)km㬵y{7J-]Q1 K:! ƌyh ^bq\^DJwǎ)8>;<3~kF?ߵ[Ptv+_[6gߋϊwiXǽ+>4 C8u>#W8)JyM7'v cŇt~IGrī5 )vsc:ꉴ@zb h9GiS%y5Ch+Apq(̱./JΝ*Ρn0z+0ca9B=ؽ_So{e'D!ۑɹ]lpxc&}:;ӭtJ,&%=JtN ~tv_wD`ڱm ~\Pc!QcU  hјդA||drZ%rBET,s;F#" /=1 YN::'*by,l%o2I9ƀ 9= ۲^oj=T[:́ /DŽy;9+?>bI3dD$qm{;a1c&y:oLPr{75_orz1x ,-YP;4NwG]Ğ6, /'A~Z֞'~˦?͵镁y]e,5c"6/G/`KOFUŏH=WSa`6Т4E;mKXdo(7 dvđִɑ j\9{/o6wbo=.!-ɏlZ:Ac:Ö?MW{+PQ aq҅a?Fado\[p ,^zDkw\Qi\M0h?/;&ϟ܃GF,:j(&]MoЫ` Hd qS |lgGœdDܗEn _ ' 79"k.<8TOiנ _5z\̎cuC@P[?*^}>+?`?viG۞M3qqOE'eX[e|X _5i/:Ҕ#P'gF~~P>RLiO6|}OZlȒmhjWQO/ } -{&'bچK%($C0g3t@rɝT+qS[IE^ȌiLӓ)qD]!ɥBM>חTpE?"4q`촛+n t #0ABqmz6.4ݯC 4y!|4oDST]2I1T&W%e.6:,\2]b3Ŕ]ꃔIhjCJM 0?iH&48A6[<xbt8SvH>G`'0DRL +PI 8 nA9SLY?Zly D{{(dж*i%JsuYt-Pnf?..DK3ݹ ?t$!\F3|"&EMR,!\Q_{Mg"Q>W&+V LhGEIΥH"vȲi!>HctdFVvtgvj벂v^ݞ<,7_ M뷹"-S(:.\h엱@^>HWSlf\=~{eLjý/k Kgbg`brbY] FxS&v^ꇙ=}FXuBG{ROJHVv"xVe!V: #).k|1{|tNCpT2&en(3(u&! ! !` z/3Efg ZZ,h-b^ITi%%T\/:4L1RlLnoE+˟wtg/z$r dB~wI'#90 J<>249:i/؅ukx E>|DBO8\d‹{# >nGhZdy>@wNѫi1W 'w Tט[/R I>\\dK4f|170O,Cuƽ#("B(&>kʿm$Sl7I9!p1W3+@}ak?4>سS|GBG!W8M殦ixdt 鈞~oL߿#1 9~5w:Rؿ|YIgRJ@մ5i~dpϝ'I Gqs|N֚ _EBÅf 6of=oʹU:KK5:Y!6<E2^% N^+Zޑ9=*=[]wTZSf./Y Kxawt3QZl.15RiDܺ`hހKz1AP.:ĸ)6XơS~T!'EDC @_ Քs / X;Xs1=+c0zL1{~?RNO ΒYQX-Mc5m `A)UuPGn?~+ȦBGP5TZUd@GhȸsqC`1N͍%׳9y?,Hk"였haF'y>'`j)[I?_\?⋦{;V_{V_ 4ժh8%p4."7Gh,64+td\fM}uYHSx$$fѨ@xtぁx9f! xi6bod̲T՜Epx$|S\&?SX_=7G< ynxdݰr8DLM?&x=Y":uǔ`G 駧U_'4կ92vmZQA"SY#%=WmiKIJ.ו;.ѡTX]m&ԎАKH2q~Zj'Y]дxdm0?3 =PK᫞-?)p~p8Lo#"- TxzVl̸ P4!)8:4_c+Qۗ 9?s)#ܔq}G<Ӓa^1,.L'{)h)il\+@ߏLt/FEr8%-ƩMG%JƳ4_݇W6Zv;_ p/^l\@ pc l <Ή5J+gosr8d]+ňLO"+ =W0pЄV8m D>BIʩtrb~<=ҊjjbK7tF#?mnztGUXN z {c?ꐯnZqf_%T5F6P+&˨~3} EzseW]M jao얘Z.^O-W:ux(zZ]k{s!~C^iת͈_e^1Fg\CJZaA%vxE 8"+PVM"v xK(aSFAo?i7Sd,v8-[b\EFsI 6癙+ O{L{@VKIXHVpnR*UXd>ʿ%΋YiqeFZDqF! ,imv{2:|Yn#AFh_,H=XŬ;qb"fkcX`!K>q|%l])lOS~=c5y}:rOz&\ŵZ!qNbY8BtbS).4=7'XK8ZH]-TFN? .V?[[o"}_V^uwC&S*ier8LPx|W*V,D# E kDNo`ӅFEK̪ W-yW7i}D1t"SW_WZf~}5Л?[CuKf.m݀?o8 *SQ*\9WJQAU1jP@1㘽HLn,"@:[<4R k ?ִ5@֨8ӥ0D@5;*`m;h첀%u裠g"uHtjq`o(h,-%(Q@N zŔ񡸏iE QWN.*&T5$nj0@q y~U3OX } аu\1T?%I?ca L7ȟK~$eWvlڂm$H>:php,&'%ܰ_BBiF8#tHn\6zjeN| { u@UG۵ܢ LJGL,Rz5޿G&(8O%^ړc1j8. tMD B sbj +Ux0XV\o?O7x!Ƨlv4Ј!Pſ]Gׂ9c_1ecݺi }ؙ~`(83Iʩ3Ѽμ\fD$E'V%nFC rC&0X GܑcbXB]]]i(MY54뉠4xXY HfҶb- UYҸI|1R) 0ʎaqVv6GhG pDy; pk=vdUuJWϰ2 GYRAV{F#kPXZq<_G29)uCFZb0[06*Ow*t˨OЄcNV#^Y=q >kgH . E)"N\\SBO=yxDp?2RX*Zہ F:)*#ə.BDFzme.Ř4#ڹ`dzk)oĵf2WU9EXS֚k8\9!فS;W|z h* 2c/B1>I}A۝DaQ<k 7؇S)Kyvٸ#ZF"*:,1٢߭.GrARSlhg6;Dv}չŗQJhUbC/7|KU4.PdPpl;bgv&DW6)2C@8A-ȱ4<)v\e먍izP:Veb.3rNGƙh=>+ޝ3a1gɱ]9MNs:+_Gcn;7ZSjGi )'! !`rjD@RBPMBY0LIP^e݇̉]x>5}QQ#Ŵ lZƋ*VQqV0c^@_:rą+qǮQeS2 =I. / Vz 4:,*7F="Ujs=\lpxcf/Ynd#;(P\i9w١ ]B3V.SˬO3N͈GcIp >®m͜M܍`a#Bdv4\r'8(yGCdnY$'Jjatph_5H㷂~(?C92>ko @~81 F T :?%V2~ͧxXxYӛvVDh{0XJM`u0oUW}0Ҹ9Ӓ{F7 (&ta:~;T-1HwC[”9zc"&c*tnmſ;ȍeG^ZB@Bsƌ|3   p{Si!xG^{B䇱$*.j7נN4TI503QTȫ-% Ǡdڌ%h -m-v3\C$Ftʬ G\%ndxGgɩi3 \|0 TY&%WtjO`uɭ@D&)՗PD*%eBy"8FS$CpR/ Sʵ0֧"8L:,<xbt8>RvH>G`B_y(ZIeű'{pt Lg)I FReҳ:RmϔM$Z6 j듊Ҍ^5mk%ZUǖc~ \zhBBsxQ#È jcf=5n و9.^Q(,`eu#kxor&/ghqb{i8n 1.;E 2E YSqzUV3[Ĕ.? Ih+K llDa'B&{z:z`Y3 n]R+ni<P?ƒ,ꏠ!x:䣍 CKǷ=< UJ[ޣ k3(t&! !`mjVB@B@B F6M VĪKG(74Jۜ)L$61K=W )bcL_,Ts)N!Knl+>ÊauA'Zdj[oQw %L["3XٸGC42 N]]ƞ)>ho0y4E8Zh/Ko ʾ,nv!;2.Q@-k/+(v^bl|&xSVP(q>KDS^>C;K EcHHIB"`|\~nYc'܋1~P5bXw$@+`d-#qׄKK/1H } !>l;'֓ϩ0dko2u)d2m갨//$R.2$Uv $$n05jx^WXL!bxM Ǣ+"9NPY}ҭŖU&l{vx9q3m5L|9@#ӹ!2Qoyd>JOx8o$2'"QzӜURܽ~;(pج67&YLyŖ#'~Ovx SKiair='oB<|3ǣ[iC9 7{ck,I'MUal",%  m)ぁx9k& x56bJM,OU9d%E2ŘTdz㜚)1g1e#gZQqT0u҃R hz0BSpp7 uhVt]2+cA70Cyu!]pǢ2 tD$nlM7؆`,~f${BS=ڢ/qjD`1')O8MqtoLZXݗp^?݂ãqcm%Xƕ Q0)IH@@G\ ~O\(KH\j}!hz ߊ +"vjU]ēG!M7 獦KɳIB)u ŘxLahZ(TitqW`fHaL(F:?(S A,ʁ89BE}dn.Ê79m|5j¨iDDsUX.MtJSH&9hU9*U|{;k @K[sWm&K f^',L(.t.a sȔ_sΙ(9UW"e Y}6^zD$Jk{i%nF+4k4r9Z'#w/(Lߵ8{ȣA۟6Uby"}&MkXiRDaf"SvC"CJC~/ 4ۤdeDw+TUXw|˲[*=Ǿ HzeB+Sx_,hz{C{IIB:!wlq.βf0?2q9;gn\\(*^aw{SࢩW݀K%)]Dfiѩi ѩɌaZoi } GN82v>ҴtHDO+vuoZ)"lγzˎSB@ֿqs٩oÂ6tf K\B@BA yΗOH QB:ҴulLjd*Xښi?h9DS~KsԤ:hA Y s""ވ(S        [HS[u՛U{"*D:H  96f܋}xz<3qg:)^dxQEF!1]Oc |s:=(}PߜGܜBrR9r-%-XXZN0E#>z~3*e5Ίeuq;q`B }v c!͏-9*q6ꍅ{v"U XgѧwFybwciSaDp3u Ut(%<[k)i-d ?Bh|6wIfgK5 X뽦cu܁{<?rj#ye>byQ@Z8~Kfq5^R0E=uS!va[aFFsyO ZtbP 6'f㏽Ga;ɩ î-?"e#ûv?3ka֠YwD`;LP_yrr:z0LBC)-BIxDSt\!&M%7pk?NO*:{G&t}S "6 'rʊob9ͫBۊ#?׀wҕug,{II9sΜƙv\1(=}bBBك>'^Oáp%PAنE #KICӨEii\p]EDu̞0_yc{WHMR8.{:7OO U{ .C3nj=Kg7+rz^I. * ?2tP'qAbJ7fS>Jk􊩋6cfi0qO~j&ĸ餽>hII i/ #~rx>؅MB,9'[O!ah 5amPYS1W3+@}ak?]0>سS|}BR@/B78Ms#N}͝TV##g"ɻ ?߀qg'5 nZʾ smg~u'hb{aDZHhoѝ|R{>_)ja~w KHGS)s07 q''=B0q ŭ4يHh0JSږE$[lKxv(6)z?$+o,&/<|c)ƘF"(;/Q}"@M>m܁^ , ^{Xp600E98R>">. z퀀ڼ~TI=s}l(TMqoٮ9v+}тzbބHR:5\eʩW"?>Ih|:S0֑%.VDT|B]gZS `H76,.}yLFZOS2'c8"O!{ҞB%tmkIܝ}ftAyi1/0"j OV+\%gLX0 IT}7v-D}߇>,áEB,\! xTvdSJdE9Bˠd*|8q5y΅Qf(3i]RfS.5 @&)}_/AO"EY|Y8+n_,YrJ`ylnіa [ Lqb KE?rjzaKRarFvwZ'+5Ǐ /.p.9vG9m"LKKxZ.63}ƢcXixb1j|dA>fى g0~dbȄص5mSM7:Ehi3#sgg-?/l{.v.Z YQ-YLX(hþdR8wcOb%E8cY|l]m9>4Nl߆fgs C ")!I-kϙiB2ln$)vHVS&KđKEymT  2CQ(;/O9 @(4},lYLGh}ĺ+hp \[5+æGGWwo%'[!G.}}t$ŚW͹WH_Յ8[pO.z=͞I.$F0w vo8o=_`!|=Hpk0ʠX} {y =녀ӎ©w ; Fqi[ϓ;h_o-oiQ(\ϓE'74M IYTEo5ZRUiWG-2f2ޘ[+2% !(ZE mrdL֙G, ̍T}/s+ُ eրAr|تVZ4N* gc4|,߃3AGU0j^L@QCVJ? gD>Ĕag%ے/Gq<利FMձ&Zvcx8:j'h>E|x:ODF}* hQFgH*.}!M9<rNK$Y|LvȴŻwWTEd\PX(w71 I@L'OגK ADگu~?\ةs1wѩQS @Wh>7*jAAqdǺ:(%s .G**XMq Mu(P!,"Q<;XC!ugO6hů+kis2^vd<ArNVwE,;XwM}І*-d5%^Y;a>&OP7` W!唽",'e!URpM\i^[<1?װv1M z {c/:䫪3g~gϲ]yٙd2Jdlcw˧Sg{zlj-jO!#sFȓ;`ZOwʧPf~ےe9srǙ=_kmzmvۆuڢ|wڌUK x x6:%^[KWjA)ɐQ_7c8W<Å;&B\# :m͕t]1VҢ"NT@?ZuF'ÿˆuu!TŸla"@H" ߏPfJ\riﯝf7@*?h7J,"PXC d늰Z\҂RfS (A'prؗ[Jr ?)ښ\bUA j6UJSF\:g!tܔ9N1eegD)n,1ҩpMS~k#3] t75Tt}Nhou^۾Et QJ;u/T!"1rڻ[ eN$A)9}I($7jM!$͌v:MIkZL8fUT06UXh7/|Z|7 )UTr; %>jMךѥ8 uy? õ:4_ދO+dYsk[6̈́xd -ٍꩭ}chZ*JV)\oytGEWMB   _ 1tv\<(( GQ!̑3J9y+y mb~?薆j\9 H-Ǯ爑"ڨx^;7 ֆոHbQߴ@^!"@ne|Wiy[ZmN$#ր VJ;Yʬ kPqI';M\fZihoDS̳QH:ٰ #P^nG3-1;L DiJ`ɪal o$|jV"c3Bs Lh%HRb1LKA` fE{D9*T!! ! ! ! ! !-$[N@Lzr&Bm#=cjF0Upf Ն`;q#S:%Qd'{BB}EWDP[]ګjmW[ZEoVAHKqeN@ @V2Yf{f䛓sf΄Iߙw眙w5sߍ} .+ֶ `seHX,eD2Jn oΫ+8N (]"B#>C1׏zM@{RP( N= {"Fl3F7*2s:ebX <.s#ĖA )h`f&2CPsk tqN_v  iS=?=]9 en_кQ""V^lD)@nNɩk/c~\E B@!-ػi-CwtmtqS&Ie2ۄKu'"?,k6i<.e98bij؇'C1wXm Va7-uQ6}j]׷rGv+ @O@@%]l/ >IlW|-^hnG#;U) B/Tr\g?ឝ1PԜ<-:=T=4yomB@!PHTrw*l{Z"4_,7=Ti E B@!<뾈]! X~2.Z|~;O?>/2E"g8?쏕bps18]ÐY{܃sз;Qm%3wY-_`_e'Ou)^e B@!<*9izCx$aFxx(xwt։ BPL:Rb& I+maxEō#k[D11UE$Ow%),Q-9_li͇ds qNxQ׽:HY B@!{N}D jtw6k돘בT尉e]9rVb|4& AOoYi1廷cILB4m{h^B@!P(]pca}G^8ny:^}i[\o~`Eᇃi0u4R"Cpql'5,)ļUsOsqME[A(P{}˸<5mmCS4Xʿ7e2"C]LjL5k;MB/)6[߽+bͿ8Q̴X1*[ƁK,b y b=>ρIZ?F(o%Oư($784sQǧCQd7]@6ֺ_P`L B@!hO̩tY|4R#.7%.w'F#2>q6&މS/ SMAN*GbL$"##Rն.^ TrhwhhCC{*NTjr u YT+16xs\ǡ8*AM3: ѽzFjrf|+%rb0Y;7O9bxİ_2 Ub cQ!P(퍀y-wܟwTBV].ҶH]6IlSm-ޘ1$+ʺDkXxy[Z/ŬHOX .MxͭR;B\13Uݢmቋk#nXgV͙{iqYz8IH7 FAsy|؜7ұ l[k|+͙yP%vn,(UKERAT//NN_<}f:]` B@!HTrH4-Ui;1ebA?œXE#z=\U x>.,68m3" @P!g%hVіFZHI']O8O§4 Q4XuGOeeԱH9X3B2a$DL͑X+Ӑ+`Rߝ\t‡| XߵB@!P™ۣJ[H; P}ª:f?4a)xlQ񫓢CR[I I{ru5vjmdLGĜFW cm>?X ZYaLw2._;4H5B@!kwgq^̃#;ֈ4v/D@ͺpӽ*|VbxrG5}пPǍAl d,kp~JڕU!P( "4wrU(:iG^L"2)i=AW6QxsO $d_?Ab[Dt$!uE}(M!KbYǍc;nɗ;.Ĉ8Tn'UP(?:q7i$z7TNF^NG}XrjAHHFfV6r6ϳTK>]zeu9P[UF\OW?9'Ɣy>Y)f`nAh 2SEi/oΩIַa~lV$b֚V!تu08s,} ݀ěhcz<^ `rҶ*U)B@!hj 8v?`t1W5M`ɫp>o]VrVlO%VmU_ 7/>y *xA7hsArPism]Gni$g6YrUSOX>|V`M@n W7~[wYY"*40dFF#J Ku8.^س6$?~:FCJxu5UUP("S3c͞C^bz<} [U圢Jؑ_CQjgD{ *3q>f8v1+AUDnQP(g -U#@mz@KSE+94Z;j{Aa#q`d1]Uq7|eKq[Zx-q }~r3 n@PPLjwˠ7j|kQT>;d(\.AHD "sѣp+gjЙ~I12nfr Mi)R( JNe?(:U"?˨2LZzײr9waĺ]B aXr]DJxoavLEN+1 r'::i!W!(bʧLFR8][y2}X@-<7W#545cxFoƗq~LFW6J*Ӽ'/6k?:ւ}E 3a!|J)B@!P-vJ*ʌHp94lBaUv</ 4WQtbĨ#޶2l3l؎eUņٸeC.1嘐!/E B@!th*؞Y#0j% N.{A@@pqoFM'ϰir}? mvJ*𿯶}i:*/B@!ЕizqXqS[}{˫]JLAaăKXRe7Ϊ~B@!@ONl /GZM(U-oznSQ( .@"97\fP( B@!Љf4 (A!P( B iz4AT_W}P #%oAT=cz<Ԃ )3=+t~ϸҋN=Jg #ʸʵo]_e0|ƕWR癕9R[SY:?X0oߏb;߸, B(@Ĵ ؼ^cH!8L,BꐬhD5Q4aVkr:  ~ZZE"o "#B;P&G!2:~NxDUD~Bza` q(VbCnk ˨ʦP?69ݽy=Kc`(u߽JNCZP(!-/.p~(TcxȂ_c `T:a +1uF3h\"mVV*Xosm,#`Kɂ 4@cy꠳@TB@!`mrs3[ު`Tu @[6&jw޼X*RES{vJ4o!oBځ;~eR 3'(WTYzzZ|=F\q%w#S|uXpgg&%]A*ϫq`w v 0xl22 ƈԉ*4c>qޔ?}5Ĵ{W"%7眧}EaoV) G%=}m>cTB@!aҚ 7ײqTX8z ffV,jELur;pm,I;ȑ&_ (2Iؾxl01s袐K;(9n;E#)+k3ĢpVmw:r{;a+?/sS὇YϼqX$N_$3']֋}QmpxA'Sv3n8.*8N/_s/Ƌ n7vqq @GgQ_ N4$B갾Dt [jP*H@4fiSA+)/38-G-1 u~80wrl1gJ<٢k3[Ux ݄+Ν?Eq z2RŃgÚAqHrS>WbJۄOQHOr'܁=+p'd)ǫV)5c>-1ҫVAHNHQP($:cr*uϧHg'E @wDQUR\4^(#Y$-Ԅz6H-6KK鵮-T5Z<%|q3~_CQ8?WYPJqrسP'=U}5.χ ;U2HϜB JU(Lk=W @wE3$`@;?Fo{Ϯ9ȊU] YB`RdzNALc%2q?*5ϗig\@qB `s㧤ߙOas#6.QQ_jC!h_*}Tl.472<#u+O'm I1 vm ălP6&]czjpXTl+t]J,EJHǃ0:72>"~"aC9a JKѪYc2=KQi-ie I,4B"p6fN)$蕅!rqq{ˤԝƭM6GvU>/1L ݋b9׿}kU%(w&tB ?eҴiv,=9%$ɳWx#3F#Gܯك9sĴ]YG01Q$xN *T8@ͱPǼD,.~b\{Hv3ն0'pR܋ߟջd6,[ӈ/*[4UvBg!OÖ_eQW_%Żwр|Õ ;GהPxE5=_OBpQdܒ=T$PSW} #hܵS,lIϫ)bn߲Si%w -\G_C, O~qJ^]ź'OuZj)VtAD$ѳC=9=źkL|089L=p۶5*z@xo%irGc0Z}Tx +=$jSSeX}Sū&g#XTK;  MĀ:izN6N2 .@IDATl'&\O8dYu4"5JI/2ki"_5"+5A(3_H @E^bZ]UPnQ6_}Q(*43ٞdGRXsPm@UQu/F#c \\K7a ;TP( @@L&Meg$}s&fz@]Mn[lAIE vE!j&]yɁuh{fIq1vJ{S( PVTS-FĞUN]cY^&=`șS'>Y6#1wi`T6r'Z=o:rΜlF}&]zt&L6l#YOTYgN5e։eo61トD!6RE>F\r?~wiƔW§Jv;}-) B ʨ81eNeXGLlb#^'6'rj$S s٦ONXg.&l'ND2uG/>imF2h6pmtѿ.E9ڗcϿ1@@u)4QFB@!P'VSNϴF DtJLydJ"xSlDu9aalˢć|L,3',sso>؍dNX})^߾d1gaP-k@6@<D݁uQʤP( EJrJ=13٨,ڔuN֙Sb/ɪ̹<&ia:βm$kF~F^',>Y7y=vO\G=Ro1)B@!Pt-&m )`uz;œ6=_ *zIf;LQN|X6T1drzz`]?~ `#z=j<]u°f[qVc-GH!P($t$Ji9Pouyg7mc(49e629x#^fs|R<:q/ۻl4vyF~qrd֍euP<2';e_|g5Ѭ嗌AIJ,zf4P( @ p&ɩY ɜϊO|V7o#‰ ǰM9lDgYfq",FD2FF6Mϩ5}Ĺ>٦3G@~a92e|wVA+.C^D:} B@!N% 'wN mēO3]\2s'C'Bl&cNzYsl\^w78ls†ll6'kCI|$!RP( .ߡpXgNv#lD|F$X)aDO>"s处b6s2œNvbgq*獸.o1kF~qy0>-X}B*mT"B@!P(@ Sq@ IbdbY7d:8V)dFS;<,uHWlWm8BqV69cзC6l B@!$rBioIN,%u8NIS1J`$duYx#n5ΨlWg3FlI6x%cE B@!PSY:IsR I+$ceNI'lĄs9 ]9!\}\Y2lΤݍeu#N6mNlDcخz`kpq<x衈všG0ۏhI=9嘑bǢxW`j\5xPsV2Tן?bO+|}cڈPT'6}]peYFXV1VjHӕX0x;>h->\5C&hQ,|V϶|7-nmy'FlKmT_-!ՠD@OE)E?+˜2~O\syzYfQ:ŒNDqA2ozDz8یlˤq.~A2c(Hfe8 }KLJG2>֩ E2C|މ) cbR`N2#,Ma80(6QAq&'nmQ=9PdAdD(zGΡHMBdt024ݍsf?pd(9O^S=.49熟]'.n1ay&lH+^eH& -Ǭ]v+}HIҗgc?2 ^6o=jZi>F=heЖO|Feh@dcäeVDd$)7eeM&%yuN 8Nv./8^ϛfQimFⱇ;Wܻ`?gqYgn<*<ݼ}?jxok0r(z g:s/dt˧-_CܛLIzl9aYEvֽqG2źgV}S2t&,q: q]α|HNMwCSWg%"|=FQ`j:F^x;c 1ⶁkp;-~9XwY?G]5PW~"[Rq@$_azl\VO+zaVzjj߮Bޱ:|d8QMcq[Qun܃V&:hsČ0x`Ղ> >?vg]?|iSgcxVW{gE -+k˖l9~E 6r\`z ܍O@|\, j>->«+q<N2f$srs?qި}\c݈6>=1=Ae†t&,{lR Ce07A1 /i^>wˈvV"62S4}v\tΎNDTXn,bp\ܣ+czZrj!H(QspcHLZUgT#4'y=}t4BpȢW $"I9XŕT[;HCZ.XZ Jſ|v%jqAqYSqUm3oF'R#A<' fµXJ?61:oV,>Y^P^2Z*gCcm쀯?Xnsebğ 2ᢟo,ZP(4|%t2v}/pe21qRWY]5{oܛpTͯp~&R+ )S:KWoq*]On^=oI3ٸٶUN{53Lt&yƗzbR5պL@~PzQtt߁*dq9|atiHZ[jb[_?q $ހŏI} ;kig;g†mYGêlؖx_|&^n}kǂdž~8@wEWrz&法檉l2yx̺7JHWNmd ̅91_dq{.jy ]k_itF:N6"}OqcueDu:ۻ&rSnޯ["&NNyWSٓaݜEQ)'j*h)>/9ȊI= h:"H jHCxe;ŴgqZ/j Ï3MX- 950&a |ކ CH~\-fj>iBxbHC1!-mXW`ȻK?^ySxz5>#' p٦[(Pr'u(ou8% ,kd?(B992$FͲLHYf7.H&:]&{ 2sˆd3ѱ@6Fv_:Cٺ'CO{;Wʿ=Kvl*-!2W$dhl|Z)ʃ"8$qϐzZGVy7R-%|q,^X xŖ/q &vm&7ߎCf"! - Q?ēJ'ᑬx>Ϣr^#ymY2Z+7SDGG,  9W^l)VɺYrLK-'}.G>75Ɯ}2;ɬs c8v-c$8'یdqFe8e?i$d ,'8kA>rwRyW*kz&ǻqqYatg"<(8}o1r6Xq>?!:ڬԘb<0^`X&ZגxnۄsفeuNʓx1#zA%'CSŝ⯆7zVxh( Bܰn, }E_dؖ(Bm{'>w^;P<&f7jz>Q]e[nta 'm+} YDqse~=9 N$ۨ16S^f\^oXgNutg9aG22g;s:VXfN'G$]6qZ3im0T61<|+|]bmH}TT$ڄ@H"zZŝ!8f,{Z큨o#jgVҴ`QS@*.K⡤|=QC04 _&bE8SWz^0~UG ]6~W뙜*Axz0ݼZbMW[3^ǯLf>ҠI[+؜5A f?{(ٲZ-}O/#E8Qw@׵qO*B!p6)ىdNcl7'(L휀R9Sy9e~jD6"3m,{#\Z˧%%Yӗjq~XIf]l7|Lcu!jDuбED:H.KXzr2nKHcg#(FfFQwqcTFo7°P# =5A(3}GcETq/k^z.A QږGh9bZ7!`;7s0?2tH@T<)xb|sc~\"ː=5W[:膖sgᅫ"}14Ĥ`X;ho=/7Pvƶx啋e;y&$6߾՝3}%yi yY8م5(nW.}~z(g[ sVUMn8KJ w?ZOh5jC}*:+VS;rȺ,kN]Vק7Hu'ǑLd(F3+N]ZO_%e? Y9>fe.Hce?Kgm3=Y0ݾe9r'\cpK.8'& kR'\bph'h([Hۤ|׳5=jKG{ qќ.gk}kC@r˿Zag82E2ho~XF )tl6۫ońigR|yi-Tͫ+STxo:>EĽ?@ZAѮZѵ^^E* |ude:qF>:pvnV(N܆l7FDeֺ/;{F7vqF˱,LHMs,B$ 4aզ'Ht~PϜڭ^U{4%<:CSeXU=h\98U_3\/mڪmZ'(|IW&]@?7m@`yq?VBsve2ۄKu'"?,k6).bΩ2N8JoueuHo'8xvFvި Me#-vX&\l)؍l۽qO_N>Ge >S lqq]*vU~l kb(Ny[ZկzuT5 ڧF> : z}r;*1 SUu=IN93OL]Vk<9d|deetQ;Z0F%|QYO&وe!j6Y'9tLѦ QaɘOlEpu6Ԧ(+.C^ĠOD!`ѯn$,Sl͊ɅXo}amzt&j{+}Fc78ގ 12$Μc-.?* {!)4cxSqUF!Ptsze}%N >ϜmY*~V#6Fu+\!H?'#v~y3t||$s c]$SBL oT=gEUV!P(|6ޯ׹8űe.K*'*\ldܛL>"Mjqm5Fv~ҽmrrRJ2?8կS"B@!Pj{r,syo6qm¹m}¢Nur,2&}3XW܅.F6fIfc&sJ8x?sR:qyc?9*9%) B?UI Xslu3g.e^jz-|qyŽ3G\7eo\uvѷz xJuV\>vc_~Ͻc}_JnD[WYyJܫԱ|y\(sP5 Oo{2gLg?B_ly/[GN $fe^&>N2XǺ|6llmFfT/Řmx>W#;dNr0O Yh͌FTD4=C/ǮSG]}‚B)6O"o "[ RQ^zF0Dž㾹-GVlj%XiU(x䎉v7ULg1:#ȴ loYǪNSH]֗ܦ"t*dstb&d8Q\벟pU.!˳.so>9F6YflF728i?gF2dXqR+; ?oEd=qzkXuG*H~[oo!-F-&C& d%ƸYkh`z,b:حG<.H&c>3Tni:V|wUB@F,9:xoY3v'-$AY焄̱2:dd#./gt׌mh8ocldR_Dd'?,S2N eNc蹸4|)i#qnH!)VGx05yF#/<ێ|5W{h*QPLl~g>2C(&~RC_]W%k hMwx⧟X8$^;7[z:L^v 1|EU`E4F ,(޿ ޻ O ] Ԏ58V]f9N/^u⭿^c: msfb8mX4C[;˖IJ/Ax$k=)X-ðT̬FMKQMbMÕf/"^k{ mA̚oyv,'/.뵟tmpxA'Sυ8f  'kDۓf_7ӎo}߽/#1 a5pt)Rkk6_2qA1VXGHnK,sV@Jf^i4v&11$ cckDA"aH(~ UvO)2nLXg6Ńme74'J`_1g쮞SZ9G0"FM/nnNc&ʸʱ9HL`Tϕc&|rhfBz p'\fϊ/܉)Y*AjS !_ގ]e^T+ǪʥhW|%tOm\6o\nC&sd7#KnK_uve̸7/9%8e\leP,Ő_ֹ.K:oB$r Dxvl*3Y,gv ORm >uxl[+R6{9ڊ~+Մ+Q^\%}:S EV-< trb:m:>=1(u$Uէ]? ;vV0s4ݯ*fwCpPܲQztY|%VF'sN8^ou#mV8a{lF:و.c d%ozuv!UMFDN2#S hC‘*J13FОB8vcF|ӬQb%jQ;*?ΆN PI54oF%jvmw_n!+1KX, b]Ap, |ƧO$.΄Aj4]*np}xh9͞ݝ^w]ƜÉ{&.Ϫ%hpϊ9>iQ X_1 Er:{"ռe^Xy@ S[>Ⱥ6] zٯtz|ܦ؞g<63Ǜ^';'\9re^8A%neS=]ڎ{%`x u6fcH@Ourߕ 8f,{Z-A6ݦzV uUZ9C1P%_aI flԝg+ό.wڌ/fǠj3Ҭ\s`qB%3ĩue&jV&T{ゥwB8 j393r-%yGx JKkQY*1*% e$WF2xl/3erl˳'s=>l3uu1r8e;ǻΈlX7TH'(s5b19kģ Hc2ª;mHcqBT#^?CW!]iF?F6)b'")a= Nޅv;Vb*3esHGS\@ͳTqWTE1Q<n/r?i%hF {\7Ρ *>{]{Qx h8 t9yyn ),= E54i0xPE>>?rqO'<'Cm,k:[Q)Շ&⺜4~5piZ3W/roK$\[Qs%¢-m&Kjsoz@l9TgdkNs,s,sѯץt3e\'wa8о0kglTgY}6EdƜvTFauZ{؎}sNT81 ٘㾎軸aK5c'w5tjH^K,?I8gBqs+1TV`up^d][ZY-QJ\?o'Dz1"+1ob_Pʪ#0JO8f˰Ud IL׺Aјp`̘OGWZ MaBR1{Hxh3ٵYm%P .gxȪ']q_+aZq“3pCiB5%9kX6Wt,򬑕mt2s#&'HHׂ }FX6#mFl/Y&,F79]沝dz!ƽ(x7pܞ.U`I~0 %Q,}4cla5)}E"f})7c&>?P ,ư(_59L;U[=S<@ `񋳯i06}x@`ma' WbSzB{7c:.6Oi)=a]ztڈXuk#Y^6r e۸'d9ebxreq,3ǰ9zOFX6#mFlzt\DS"J2O.C:cr]Tca..m1[F X:]qK} l3LLJĴ3+_ź3XA* S׌~a=~ z*.&tb/2e$q2`&~|ms13ps)7Ge'7*Dz7*Cq# ~q"8ڷu/+1kho*i1*zWbYc!;J5ihkdQLZ)3ldgb0Nl#2:s ziTNΜ/"),#0(+}#34lNؓǮh\/"z#Mes!`l3F7Ɠux2ی,o^LTH!P(:]<9 DtYN,/umF:>/g]>!U88^!~`Yr<ٍ6ꉑmm*P( )mMNB:A[I6Y'eFv3ܮY].ŴweN26U<{'_-} .9d B@!PFLSN|o]{[Oo#29F͛|$F8|f'!c̲If˱MϹEζV<"4}$cEpw%H!P( _'x_m(g1ę6Y8͛c~Xu?Y)!==?gZ#tbƬabA!P( clnD(ljM2zY7cdmߝbۘ9ސQhhWFB@!P(8\3̉^xXlۖmmS&N˹>rjèh#S#N6:qOM-'ޡH!P( Trʭ'vq%D|}ur7{K1fX8#@Xsʸ˜d#죖e,ϜU˂ B@!P@ )5G'o_I-_f~33v+e9Fߨ~)ޱ}E:X6esֹ h`SKb&TڰJeT( @Go/ӖX.x@@$Fccdݛ &IWSefTr`^CR0tx4ǫ=>+jY,kؕ=ƺ 0/~KΚ@Q kќJ@S.j5J9Z5k͹ܹ>jkv[W\~{37 Q D(Ak5um X>ssRATw7Rk?~Z'?7EQ D(p \9B4{4h̡㵪g읇Sh̕^V'gdە>c^7}[2 D(V-S18<{|u<ߩͷ2α@Qx^ |y{[ v+ĺSzP(@Q`C[hN"5Kbߧܛyʜ* D(ޣ|7[jN ^d ֬5׮kſ]yv@QWm؜"wff?k}L ?fk%@QTVW+9r-7~(o_WVgV;uO٣@Q WிQҜRf ^AVQ DRYW=6U*bPi@Q ܪ{a|'<9~4U{^N@)ߴSj=6#AVۼ ; ۯʧvND(@{s{GjNWJSmu?\`/-1gD(@U;=ξ[իX}Fy#^'Ůw¬@Qo(6ynOs,ULՇg5ΎF(@ح7q5oT$RN6zF;"NJQ Dw6ӜY=xQ~(@qoʜ9iN7%JT@?(GQ DVT~e7eӜϑ͜?x{WLu@Qon_otuN;/qޡ݃R9C |b@Q ۥ8y>|q_̦9 t cu;GϒQ D( *:'#7z!qj6RgE.jb:'\ȈQ D >w9ESa׹w;V %+1G9=#;Q@7/,W8]uzqN w,wVhP+NuuxG(ޣݻŝw9ұxa?0yOcEpӋȘI(~l sp35G/n.+ƿjO#\Zb8f*/5G(>ݻps[1nG1y3\YqѨ~=زMs,U@=,4a]"bU[k_T7SL/L^>4T?;r;Vsb@Q{ҹV k8W9Gv >)_Z8$3-eLsOdij5Z7Nkn.jŢAsi-i(GRGL Q g{;YŶQCZycO,1܊:ǪҜDnhxhXCEÛRͣ8:aG#|= /A`ùz烣@Uޟuq:,pzpX=O<X4q_/a5w;Ҝ3t{Sq:,NZW^ 45_1 hJsāigU7rQ 'ދ+Y焻K} +7{^SX|X>]8.|G !f6]u$a|V/ OnKG]:O_vk[G(ރ_1[ᕋ=\H|a=GFW6_P=M,&o^+zhиֆT>guלN;kjWj](*w<Ǫu_-<ͧ΃ݯ3*A{.5YrnS$tsYs+!K V/HxqY8=§Yx,V<ñ8W@x =]r 㻅 vNKqig,swyXĥ9J+^n}(jFySK󃱕 T9+W5Q3Q D->ùù|`Ywƶ:O'9<%9]*/|v/cԺar5iЀ k=|,%,v[qc0'~lQ {~4j}ۊ;_7©q,a8YuriNwK;U@7c 'KFpnkqfTƖOsëy]m(=*}k]sUn|xrFGXUL-/q^ 9}5i3ЃAVK±| Ղe5gczl5!ŲY.偫1:'踚? D(>+39Ίs߭7:߲Gsݯ9רֹ?iNJͽ5cj;M(Vy-[1yR >حk>|(GV{A|xي;Gue5jU{l5iNGgQX;54jm8aj?`s`3Zy+s%' D(p ~k\d+q4޲vVgX ⼖42nK5CjhMKfsj>r,XV}0zLX9oy<8 D(lϬb~ini9zU^VxպQ D{UGywNKk!K\}09xjȯ9W^Q9/ͩ|O &H=skN롧pNsR90V9y}j 93@Q`[5ᰪX~-K:sX)wup4GrĹ1ts4RQC#y=+hטZ/3g>xWG(U0ʫʫ~}ӜctwMqɩq_OU/_u˨0W~zHa5=x/-xlQ ogV\ 9]1s/1:c42]Q=():QyoHɇ4ΉR {sbӨ~=9\pQ |n GZ*?|x]CZw7q ?⨟4Sytӏ.XVy_=4^ͧǝ׺ι_1ۊ;_>x@{}7*>5;\cVC͋>ٻ`rGc;Isz$G;W@77ax8_=: 7^KܛVwֹ38#~[? D(Q6wmŝ/nuɦwW}mӓK )chjYHsz3M=kjl~dG L,{PΞQU< Nq ?%\]<\Q* 01m\W{Q 5΁e5_8,-Ms:'7P@7ކjTǁ*.|9Tlan509?Ύ@Ug;]5oϙjk0su$GR`ެOl\K_ɣv*Gy+~ 8\g}.. D(h'sα XϭQꓷz>N`aoCWí.㤞Y k5>xe.. D(6NQF5w3cm}:zqt$GS`r*~N sWq3'wr*7aqİp5翳笠(@x-fY[8Vgl)gڳ7ʽTn{ <`친 kܝl{XQ |V`F5_=Ղ`)k|?xcu$GV`^8]nI=wvub@QXi-[wTˣ8bnZ?|̞V.oq;qp/_k|8,FNgO @H ,7VEY]:m{Ǒ<>㈹j=~:#(zvy+Œoub@Qjƺxfŵ%}GmW8qVQ^Wn{̱dsquNݨkv9@Q iXsk^珸_!l(M7};bs߱֜nY.֘'@k n>M;sk?@́w:#)zX.K:'Y vɋQ |$Njfb_Kzӏ35O8#VY^[.|d{:1Q DK*l:xWk8ռO-2 ܏{Yn'u؊{npQ SFjRh/ ukvOn}Ӝ#6nQW9IxT9kqS@g7V \Ycr;N[[OGifM[kh-ك/=ߞ@x Sη2(y3Z=^Bwy+n.J;> Q D89[xb|+Nvo>u%+?'~N'ؚsT> D(FSY|ƫ[M?Mp r_rf1$\9%(@4u+Y VrŞRCX'I ܠ+uʑl+sY;Q DV.+swu\>d%8SW>? D(nCSպ<';|$N8[I{Q DTU7wOػ.|dJ;P{}oNK. D(VK4x{؛_:7J;T9sS{pd(5ZS699{8-]bڴPmJw[T9RQC)p&#^k$GQ2uQ D(p.,vs/=h-]+Kk=5yr(@( VZ|<$p5>֯F(޷hTI@ sk8GQ ܌o ¡4 D'Q ܲ7- v{kLZxT Kn(@Gn!uQu5>+Q D(pZNW ڥ2 D(NW஛z|L"svyM3cQ#*HQ yY1 D(p |sU8L{IENDB`httpie-0.9.8/Makefile0000644000000000000000000000416513022157774013201 0ustar rootroot# # See ./CONTRIBUTING.rst # VERSION=$(shell grep __version__ httpie/__init__.py) REQUIREMENTS="requirements-dev.txt" TAG="\n\n\033[0;32m\#\#\# " END=" \#\#\# \033[0m\n" all: test init: uninstall-httpie @echo $(TAG)Installing dev requirements$(END) pip install --upgrade -r $(REQUIREMENTS) @echo $(TAG)Installing HTTPie$(END) pip install --upgrade --editable . @echo test: init @echo $(TAG)Running tests on the current Python interpreter with coverage $(END) py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests @echo test-tox: init @echo $(TAG)Running tests on all Pythons via Tox$(END) tox @echo test-dist: test-sdist test-bdist-wheel @echo test-sdist: clean uninstall-httpie @echo $(TAG)Testing sdist build an installation$(END) python setup.py sdist pip install --force-reinstall --upgrade dist/*.gz which http @echo test-bdist-wheel: clean uninstall-httpie @echo $(TAG)Testing wheel build an installation$(END) python setup.py bdist_wheel pip install --force-reinstall --upgrade dist/*.whl which http @echo # This tests everything, even this Makefile. test-all: uninstall-all clean init test test-tox test-dist publish: test-all publish-no-test publish-no-test: @echo $(TAG)Testing wheel build an installation$(END) @echo "$(VERSION)" @echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok python setup.py register python setup.py sdist upload python setup.py bdist_wheel upload @echo clean: @echo $(TAG)Cleaning up$(END) rm -rf .tox *.egg dist build .coverage find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print @echo uninstall-httpie: @echo $(TAG)Uninstalling httpie$(END) - pip uninstall --yes httpie &2>/dev/null @echo "Verifying…" cd .. && ! python -m httpie --version &2>/dev/null @echo "Done" @echo uninstall-all: uninstall-httpie @echo $(TAG)Uninstalling httpie requirements$(END) - pip uninstall --yes pygments requests @echo $(TAG)Uninstalling development requirements$(END) - pip uninstall --yes -r $(REQUIREMENTS) homebrew-formula-vars: extras/get-homebrew-formula-vars.py httpie-0.9.8/tests/0000755000000000000000000000000013022157774012675 5ustar rootroothttpie-0.9.8/tests/test_sessions.py0000644000000000000000000001521513022157774016160 0ustar rootroot# coding=utf-8 import os import shutil import sys from tempfile import gettempdir import pytest from httpie.plugins.builtin import HTTPBasicAuth from utils import TestEnvironment, mk_config_dir, http, HTTP_OK from fixtures import UNICODE class SessionTestBase(object): def start_session(self, httpbin): """Create and reuse a unique config dir for each test.""" self.config_dir = mk_config_dir() def teardown_method(self, method): shutil.rmtree(self.config_dir) def env(self): """ Return an environment. Each environment created withing a test method will share the same config_dir. It is necessary for session files being reused. """ return TestEnvironment(config_dir=self.config_dir) class TestSessionFlow(SessionTestBase): """ These tests start with an existing session created in `setup_method()`. """ def start_session(self, httpbin): """ Start a full-blown session with a custom request header, authorization, and response cookies. """ super(TestSessionFlow, self).start_session(httpbin) r1 = http('--follow', '--session=test', '--auth=username:password', 'GET', httpbin.url + '/cookies/set?hello=world', 'Hello:World', env=self.env()) assert HTTP_OK in r1 def test_session_created_and_reused(self, httpbin): self.start_session(httpbin) # Verify that the session created in setup_method() has been used. r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 assert r2.json['headers']['Hello'] == 'World' assert r2.json['headers']['Cookie'] == 'hello=world' assert 'Basic ' in r2.json['headers']['Authorization'] def test_session_update(self, httpbin): self.start_session(httpbin) # Get a response to a request from the original session. r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 # Make a request modifying the session data. r3 = http('--follow', '--session=test', '--auth=username:password2', 'GET', httpbin.url + '/cookies/set?hello=world2', 'Hello:World2', env=self.env()) assert HTTP_OK in r3 # Get a response to a request from the updated session. r4 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r4 assert r4.json['headers']['Hello'] == 'World2' assert r4.json['headers']['Cookie'] == 'hello=world2' assert (r2.json['headers']['Authorization'] != r4.json['headers']['Authorization']) def test_session_read_only(self, httpbin): self.start_session(httpbin) # Get a response from the original session. r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 # Make a request modifying the session data but # with --session-read-only. r3 = http('--follow', '--session-read-only=test', '--auth=username:password2', 'GET', httpbin.url + '/cookies/set?hello=world2', 'Hello:World2', env=self.env()) assert HTTP_OK in r3 # Get a response from the updated session. r4 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r4 # Origin can differ on Travis. del r2.json['origin'], r4.json['origin'] # Different for each request. # Should be the same as before r3. assert r2.json == r4.json class TestSession(SessionTestBase): """Stand-alone session tests.""" def test_session_ignored_header_prefixes(self, httpbin): self.start_session(httpbin) r1 = http('--session=test', 'GET', httpbin.url + '/get', 'Content-Type: text/plain', 'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT', env=self.env()) assert HTTP_OK in r1 r2 = http('--session=test', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 assert 'Content-Type' not in r2.json['headers'] assert 'If-Unmodified-Since' not in r2.json['headers'] def test_session_by_path(self, httpbin): self.start_session(httpbin) session_path = os.path.join(self.config_dir, 'session-by-path.json') r1 = http('--session=' + session_path, 'GET', httpbin.url + '/get', 'Foo:Bar', env=self.env()) assert HTTP_OK in r1 r2 = http('--session=' + session_path, 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 assert r2.json['headers']['Foo'] == 'Bar' @pytest.mark.skipif( sys.version_info >= (3,), reason="This test fails intermittently on Python 3 - " "see https://github.com/jkbrzt/httpie/issues/282") def test_session_unicode(self, httpbin): self.start_session(httpbin) r1 = http('--session=test', u'--auth=test:' + UNICODE, 'GET', httpbin.url + '/get', u'Test:%s' % UNICODE, env=self.env()) assert HTTP_OK in r1 r2 = http('--session=test', '--verbose', 'GET', httpbin.url + '/get', env=self.env()) assert HTTP_OK in r2 # FIXME: Authorization *sometimes* is not present on Python3 assert (r2.json['headers']['Authorization'] == HTTPBasicAuth.make_header(u'test', UNICODE)) # httpbin doesn't interpret utf8 headers assert UNICODE in r2 def test_session_default_header_value_overwritten(self, httpbin): self.start_session(httpbin) # https://github.com/jkbrzt/httpie/issues/180 r1 = http('--session=test', httpbin.url + '/headers', 'User-Agent:custom', env=self.env()) assert HTTP_OK in r1 assert r1.json['headers']['User-Agent'] == 'custom' r2 = http('--session=test', httpbin.url + '/headers', env=self.env()) assert HTTP_OK in r2 assert r2.json['headers']['User-Agent'] == 'custom' def test_download_in_session(self, httpbin): # https://github.com/jkbrzt/httpie/issues/412 self.start_session(httpbin) cwd = os.getcwd() os.chdir(gettempdir()) try: http('--session=test', '--download', httpbin.url + '/get', env=self.env()) finally: os.chdir(cwd) httpie-0.9.8/tests/test_windows.py0000644000000000000000000000200213022157774015772 0ustar rootrootimport os import tempfile import pytest from httpie.context import Environment from utils import TestEnvironment, http from httpie.compat import is_windows @pytest.mark.skipif(not is_windows, reason='windows-only') class TestWindowsOnly: @pytest.mark.skipif(True, reason='this test for some reason kills the process') def test_windows_colorized_output(self, httpbin): # Spits out the colorized output. http(httpbin.url + '/get', env=Environment()) class TestFakeWindows: def test_output_file_pretty_not_allowed_on_windows(self, httpbin): env = TestEnvironment(is_windows=True) output_file = os.path.join( tempfile.gettempdir(), self.test_output_file_pretty_not_allowed_on_windows.__name__ ) r = http('--output', output_file, '--pretty=all', 'GET', httpbin.url + '/get', env=env, error_exit_ok=True) assert 'Only terminal output can be colorized on Windows' in r.stderr httpie-0.9.8/tests/test_exit_status.py0000644000000000000000000000475313022157774016673 0ustar rootrootimport mock from httpie import ExitStatus from utils import TestEnvironment, http, HTTP_OK def test_keyboard_interrupt_during_arg_parsing_exit_status(httpbin): with mock.patch('httpie.cli.parser.parse_args', side_effect=KeyboardInterrupt()): r = http('GET', httpbin.url + '/get', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR_CTRL_C def test_keyboard_interrupt_in_program_exit_status(httpbin): with mock.patch('httpie.core.program', side_effect=KeyboardInterrupt()): r = http('GET', httpbin.url + '/get', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR_CTRL_C def test_ok_response_exits_0(httpbin): r = http('GET', httpbin.url + '/get') assert HTTP_OK in r assert r.exit_status == ExitStatus.OK def test_error_response_exits_0_without_check_status(httpbin): r = http('GET', httpbin.url + '/status/500') assert '500 INTERNAL SERVER ERRO' in r assert r.exit_status == ExitStatus.OK assert not r.stderr def test_timeout_exit_status(httpbin): r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.02', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR_TIMEOUT def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected( httpbin): env = TestEnvironment(stdout_isatty=False) r = http('--check-status', '--headers', 'GET', httpbin.url + '/status/301', env=env, error_exit_ok=True) assert '301 MOVED PERMANENTLY' in r assert r.exit_status == ExitStatus.ERROR_HTTP_3XX assert '301 moved permanently' in r.stderr.lower() def test_3xx_check_status_redirects_allowed_exits_0(httpbin): r = http('--check-status', '--follow', 'GET', httpbin.url + '/status/301', error_exit_ok=True) # The redirect will be followed so 200 is expected. assert HTTP_OK in r assert r.exit_status == ExitStatus.OK def test_4xx_check_status_exits_4(httpbin): r = http('--check-status', 'GET', httpbin.url + '/status/401', error_exit_ok=True) assert '401 UNAUTHORIZED' in r assert r.exit_status == ExitStatus.ERROR_HTTP_4XX # Also stderr should be empty since stdout isn't redirected. assert not r.stderr def test_5xx_check_status_exits_5(httpbin): r = http('--check-status', 'GET', httpbin.url + '/status/500', error_exit_ok=True) assert '500 INTERNAL SERVER ERROR' in r assert r.exit_status == ExitStatus.ERROR_HTTP_5XX httpie-0.9.8/tests/conftest.py0000644000000000000000000000063513022157774015100 0ustar rootrootimport pytest from pytest_httpbin.plugin import httpbin_ca_bundle # Make httpbin's CA trusted by default pytest.fixture(autouse=True)(httpbin_ca_bundle) @pytest.fixture(scope='function') def httpbin_secure_untrusted(monkeypatch, httpbin_secure): """Like the `httpbin_secure` fixture, but without the make-CA-trusted-by-default""" monkeypatch.delenv('REQUESTS_CA_BUNDLE') return httpbin_secure httpie-0.9.8/tests/test_auth_plugins.py0000644000000000000000000000705313022157774017015 0ustar rootrootfrom mock import mock from httpie.input import SEP_CREDENTIALS from httpie.plugins import AuthPlugin, plugin_manager from utils import http, HTTP_OK # TODO: run all these tests in session mode as well USERNAME = 'user' PASSWORD = 'password' # Basic auth encoded `USERNAME` and `PASSWORD` # noinspection SpellCheckingInspection BASIC_AUTH_HEADER_VALUE = 'Basic dXNlcjpwYXNzd29yZA==' BASIC_AUTH_URL = '/basic-auth/{0}/{1}'.format(USERNAME, PASSWORD) AUTH_OK = {'authenticated': True, 'user': USERNAME} def basic_auth(header=BASIC_AUTH_HEADER_VALUE): def inner(r): r.headers['Authorization'] = header return r return inner def test_auth_plugin_parse_auth_false(httpbin): class Plugin(AuthPlugin): auth_type = 'test-parse-false' auth_parse = False def get_auth(self, username=None, password=None): assert username is None assert password is None assert self.raw_auth == BASIC_AUTH_HEADER_VALUE return basic_auth(self.raw_auth) plugin_manager.register(Plugin) try: r = http( httpbin + BASIC_AUTH_URL, '--auth-type', Plugin.auth_type, '--auth', BASIC_AUTH_HEADER_VALUE, ) assert HTTP_OK in r assert r.json == AUTH_OK finally: plugin_manager.unregister(Plugin) def test_auth_plugin_require_auth_false(httpbin): class Plugin(AuthPlugin): auth_type = 'test-require-false' auth_require = False def get_auth(self, username=None, password=None): assert self.raw_auth is None assert username is None assert password is None return basic_auth() plugin_manager.register(Plugin) try: r = http( httpbin + BASIC_AUTH_URL, '--auth-type', Plugin.auth_type, ) assert HTTP_OK in r assert r.json == AUTH_OK finally: plugin_manager.unregister(Plugin) def test_auth_plugin_require_auth_false_and_auth_provided(httpbin): class Plugin(AuthPlugin): auth_type = 'test-require-false-yet-provided' auth_require = False def get_auth(self, username=None, password=None): assert self.raw_auth == USERNAME + SEP_CREDENTIALS + PASSWORD assert username == USERNAME assert password == PASSWORD return basic_auth() plugin_manager.register(Plugin) try: r = http( httpbin + BASIC_AUTH_URL, '--auth-type', Plugin.auth_type, '--auth', USERNAME + SEP_CREDENTIALS + PASSWORD, ) assert HTTP_OK in r assert r.json == AUTH_OK finally: plugin_manager.unregister(Plugin) @mock.patch('httpie.input.AuthCredentials._getpass', new=lambda self, prompt: 'UNEXPECTED_PROMPT_RESPONSE') def test_auth_plugin_prompt_password_false(httpbin): class Plugin(AuthPlugin): auth_type = 'test-prompt-false' prompt_password = False def get_auth(self, username=None, password=None): assert self.raw_auth == USERNAME assert username == USERNAME assert password is None return basic_auth() plugin_manager.register(Plugin) try: r = http( httpbin + BASIC_AUTH_URL, '--auth-type', Plugin.auth_type, '--auth', USERNAME, ) assert HTTP_OK in r assert r.json == AUTH_OK finally: plugin_manager.unregister(Plugin) httpie-0.9.8/tests/README.rst0000644000000000000000000000022413022157774014362 0ustar rootrootHTTPie Test Suite ================= Please see `CONTRIBUTING`_. .. _CONTRIBUTING: https://github.com/jkbrzt/httpie/blob/master/CONTRIBUTING.rst httpie-0.9.8/tests/test_cli.py0000644000000000000000000003007613022157774015063 0ustar rootroot"""CLI argument parsing related tests.""" import json # noinspection PyCompatibility import argparse import pytest from requests.exceptions import InvalidSchema from httpie import input from httpie.input import KeyValue, KeyValueArgType, DataDict from httpie import ExitStatus from httpie.cli import parser from utils import TestEnvironment, http, HTTP_OK from fixtures import ( FILE_PATH_ARG, JSON_FILE_PATH_ARG, JSON_FILE_CONTENT, FILE_CONTENT, FILE_PATH ) class TestItemParsing: key_value = KeyValueArgType(*input.SEP_GROUP_ALL_ITEMS) def test_invalid_items(self): items = ['no-separator'] for item in items: pytest.raises(argparse.ArgumentTypeError, self.key_value, item) def test_escape_separator(self): items = input.parse_items([ # headers self.key_value(r'foo\:bar:baz'), self.key_value(r'jack\@jill:hill'), # data self.key_value(r'baz\=bar=foo'), # files self.key_value(r'bar\@baz@%s' % FILE_PATH_ARG), ]) # `requests.structures.CaseInsensitiveDict` => `dict` headers = dict(items.headers._store.values()) assert headers == { 'foo:bar': 'baz', 'jack@jill': 'hill', } assert items.data == {'baz=bar': 'foo'} assert 'bar@baz' in items.files @pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [ ('path=c:\windows', 'path', '=', 'c:\windows'), ('path=c:\windows\\', 'path', '=', 'c:\windows\\'), ('path\==c:\windows', 'path=', '=', 'c:\windows'), ]) def test_backslash_before_non_special_character_does_not_escape( self, string, key, sep, value): expected = KeyValue(orig=string, key=key, sep=sep, value=value) actual = self.key_value(string) assert actual == expected def test_escape_longsep(self): items = input.parse_items([ self.key_value(r'bob\:==foo'), ]) assert items.params == {'bob:': 'foo'} def test_valid_items(self): items = input.parse_items([ self.key_value('string=value'), self.key_value('Header:value'), self.key_value('Unset-Header:'), self.key_value('Empty-Header;'), self.key_value('list:=["a", 1, {}, false]'), self.key_value('obj:={"a": "b"}'), self.key_value('ed='), self.key_value('bool:=true'), self.key_value('file@' + FILE_PATH_ARG), self.key_value('query==value'), self.key_value('string-embed=@' + FILE_PATH_ARG), self.key_value('raw-json-embed:=@' + JSON_FILE_PATH_ARG), ]) # Parsed headers # `requests.structures.CaseInsensitiveDict` => `dict` headers = dict(items.headers._store.values()) assert headers == { 'Header': 'value', 'Unset-Header': None, 'Empty-Header': '' } # Parsed data raw_json_embed = items.data.pop('raw-json-embed') assert raw_json_embed == json.loads(JSON_FILE_CONTENT) items.data['string-embed'] = items.data['string-embed'].strip() assert dict(items.data) == { "ed": "", "string": "value", "bool": True, "list": ["a", 1, {}, False], "obj": {"a": "b"}, "string-embed": FILE_CONTENT, } # Parsed query string parameters assert items.params == {'query': 'value'} # Parsed file fields assert 'file' in items.files assert (items.files['file'][1].read().strip(). decode('utf8') == FILE_CONTENT) def test_multiple_file_fields_with_same_field_name(self): items = input.parse_items([ self.key_value('file_field@' + FILE_PATH_ARG), self.key_value('file_field@' + FILE_PATH_ARG), ]) assert len(items.files['file_field']) == 2 def test_multiple_text_fields_with_same_field_name(self): items = input.parse_items( [self.key_value('text_field=a'), self.key_value('text_field=b')], data_class=DataDict ) assert items.data['text_field'] == ['a', 'b'] assert list(items.data.items()) == [ ('text_field', 'a'), ('text_field', 'b'), ] class TestQuerystring: def test_query_string_params_in_url(self, httpbin): r = http('--print=Hhb', 'GET', httpbin.url + '/get?a=1&b=2') path = '/get?a=1&b=2' url = httpbin.url + path assert HTTP_OK in r assert 'GET %s HTTP/1.1' % path in r assert '"url": "%s"' % url in r def test_query_string_params_items(self, httpbin): r = http('--print=Hhb', 'GET', httpbin.url + '/get', 'a==1') path = '/get?a=1' url = httpbin.url + path assert HTTP_OK in r assert 'GET %s HTTP/1.1' % path in r assert '"url": "%s"' % url in r def test_query_string_params_in_url_and_items_with_duplicates(self, httpbin): r = http('--print=Hhb', 'GET', httpbin.url + '/get?a=1&a=1', 'a==1', 'a==1') path = '/get?a=1&a=1&a=1&a=1' url = httpbin.url + path assert HTTP_OK in r assert 'GET %s HTTP/1.1' % path in r assert '"url": "%s"' % url in r class TestLocalhostShorthand: def test_expand_localhost_shorthand(self): args = parser.parse_args(args=[':'], env=TestEnvironment()) assert args.url == 'http://localhost' def test_expand_localhost_shorthand_with_slash(self): args = parser.parse_args(args=[':/'], env=TestEnvironment()) assert args.url == 'http://localhost/' def test_expand_localhost_shorthand_with_port(self): args = parser.parse_args(args=[':3000'], env=TestEnvironment()) assert args.url == 'http://localhost:3000' def test_expand_localhost_shorthand_with_path(self): args = parser.parse_args(args=[':/path'], env=TestEnvironment()) assert args.url == 'http://localhost/path' def test_expand_localhost_shorthand_with_port_and_slash(self): args = parser.parse_args(args=[':3000/'], env=TestEnvironment()) assert args.url == 'http://localhost:3000/' def test_expand_localhost_shorthand_with_port_and_path(self): args = parser.parse_args(args=[':3000/path'], env=TestEnvironment()) assert args.url == 'http://localhost:3000/path' def test_dont_expand_shorthand_ipv6_as_shorthand(self): args = parser.parse_args(args=['::1'], env=TestEnvironment()) assert args.url == 'http://::1' def test_dont_expand_longer_ipv6_as_shorthand(self): args = parser.parse_args( args=['::ffff:c000:0280'], env=TestEnvironment() ) assert args.url == 'http://::ffff:c000:0280' def test_dont_expand_full_ipv6_as_shorthand(self): args = parser.parse_args( args=['0000:0000:0000:0000:0000:0000:0000:0001'], env=TestEnvironment() ) assert args.url == 'http://0000:0000:0000:0000:0000:0000:0000:0001' class TestArgumentParser: def setup_method(self, method): self.parser = input.HTTPieArgumentParser() def test_guess_when_method_set_and_valid(self): self.parser.args = argparse.Namespace() self.parser.args.method = 'GET' self.parser.args.url = 'http://example.com/' self.parser.args.items = [] self.parser.args.ignore_stdin = False self.parser.env = TestEnvironment() self.parser._guess_method() assert self.parser.args.method == 'GET' assert self.parser.args.url == 'http://example.com/' assert self.parser.args.items == [] def test_guess_when_method_not_set(self): self.parser.args = argparse.Namespace() self.parser.args.method = None self.parser.args.url = 'http://example.com/' self.parser.args.items = [] self.parser.args.ignore_stdin = False self.parser.env = TestEnvironment() self.parser._guess_method() assert self.parser.args.method == 'GET' assert self.parser.args.url == 'http://example.com/' assert self.parser.args.items == [] def test_guess_when_method_set_but_invalid_and_data_field(self): self.parser.args = argparse.Namespace() self.parser.args.method = 'http://example.com/' self.parser.args.url = 'data=field' self.parser.args.items = [] self.parser.args.ignore_stdin = False self.parser.env = TestEnvironment() self.parser._guess_method() assert self.parser.args.method == 'POST' assert self.parser.args.url == 'http://example.com/' assert self.parser.args.items == [ KeyValue(key='data', value='field', sep='=', orig='data=field') ] def test_guess_when_method_set_but_invalid_and_header_field(self): self.parser.args = argparse.Namespace() self.parser.args.method = 'http://example.com/' self.parser.args.url = 'test:header' self.parser.args.items = [] self.parser.args.ignore_stdin = False self.parser.env = TestEnvironment() self.parser._guess_method() assert self.parser.args.method == 'GET' assert self.parser.args.url == 'http://example.com/' assert self.parser.args.items, [ KeyValue(key='test', value='header', sep=':', orig='test:header') ] def test_guess_when_method_set_but_invalid_and_item_exists(self): self.parser.args = argparse.Namespace() self.parser.args.method = 'http://example.com/' self.parser.args.url = 'new_item=a' self.parser.args.items = [ KeyValue( key='old_item', value='b', sep='=', orig='old_item=b') ] self.parser.args.ignore_stdin = False self.parser.env = TestEnvironment() self.parser._guess_method() assert self.parser.args.items, [ KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'), KeyValue( key='old_item', value='b', sep='=', orig='old_item=b'), ] class TestNoOptions: def test_valid_no_options(self, httpbin): r = http('--verbose', '--no-verbose', 'GET', httpbin.url + '/get') assert 'GET /get HTTP/1.1' not in r def test_invalid_no_options(self, httpbin): r = http('--no-war', 'GET', httpbin.url + '/get', error_exit_ok=True) assert r.exit_status == 1 assert 'unrecognized arguments: --no-war' in r.stderr assert 'GET /get HTTP/1.1' not in r class TestIgnoreStdin: def test_ignore_stdin(self, httpbin): with open(FILE_PATH) as f: env = TestEnvironment(stdin=f, stdin_isatty=False) r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', env=env) assert HTTP_OK in r assert 'GET /get HTTP' in r, "Don't default to POST." assert FILE_CONTENT not in r, "Don't send stdin data." def test_ignore_stdin_cannot_prompt_password(self, httpbin): r = http('--ignore-stdin', '--auth=no-password', httpbin.url + '/get', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR assert 'because --ignore-stdin' in r.stderr class TestSchemes: def test_invalid_custom_scheme(self): # InvalidSchema is expected because HTTPie # shouldn't touch a formally valid scheme. with pytest.raises(InvalidSchema): http('foo+bar-BAZ.123://bah') def test_invalid_scheme_via_via_default_scheme(self): # InvalidSchema is expected because HTTPie # shouldn't touch a formally valid scheme. with pytest.raises(InvalidSchema): http('bah', '--default=scheme=foo+bar-BAZ.123') def test_default_scheme(self, httpbin_secure): url = '{0}:{1}'.format(httpbin_secure.host, httpbin_secure.port) assert HTTP_OK in http(url, '--default-scheme=https') httpie-0.9.8/tests/utils.py0000644000000000000000000001575213022157774014421 0ustar rootroot# coding=utf-8 """Utilities for HTTPie test suite.""" import os import sys import time import json import tempfile from httpie import ExitStatus, EXIT_STATUS_LABELS from httpie.context import Environment from httpie.core import main from httpie.compat import bytes, str TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) CRLF = '\r\n' COLOR = '\x1b[' HTTP_OK = '200 OK' HTTP_OK_COLOR = ( 'HTTP\x1b[39m\x1b[38;5;245m/\x1b[39m\x1b' '[38;5;37m1.1\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;37m200' '\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;136mOK' ) def mk_config_dir(): dirname = tempfile.mkdtemp(prefix='httpie_config_') return dirname def add_auth(url, auth): proto, rest = url.split('://', 1) return proto + '://' + auth + '@' + rest class TestEnvironment(Environment): """Environment subclass with reasonable defaults for testing.""" colors = 0 stdin_isatty = True, stdout_isatty = True is_windows = False def __init__(self, **kwargs): if 'stdout' not in kwargs: kwargs['stdout'] = tempfile.TemporaryFile( mode='w+b', prefix='httpie_stdout' ) if 'stderr' not in kwargs: kwargs['stderr'] = tempfile.TemporaryFile( mode='w+t', prefix='httpie_stderr' ) super(TestEnvironment, self).__init__(**kwargs) self._delete_config_dir = False @property def config(self): if not self.config_dir.startswith(tempfile.gettempdir()): self.config_dir = mk_config_dir() self._delete_config_dir = True return super(TestEnvironment, self).config def cleanup(self): if self._delete_config_dir: assert self.config_dir.startswith(tempfile.gettempdir()) from shutil import rmtree rmtree(self.config_dir) def __del__(self): try: self.cleanup() except Exception: pass class BaseCLIResponse(object): """ Represents the result of simulated `$ http' invocation via `http()`. Holds and provides access to: - stdout output: print(self) - stderr output: print(self.stderr) - exit_status output: print(self.exit_status) """ stderr = None json = None exit_status = None class BytesCLIResponse(bytes, BaseCLIResponse): """ Used as a fallback when a StrCLIResponse cannot be used. E.g. when the output contains binary data or when it is colorized. `.json` will always be None. """ class StrCLIResponse(str, BaseCLIResponse): @property def json(self): """ Return deserialized JSON body, if one included in the output and is parseable. """ if not hasattr(self, '_json'): self._json = None # De-serialize JSON body if possible. if COLOR in self: # Colorized output cannot be parsed. pass elif self.strip().startswith('{'): # Looks like JSON body. self._json = json.loads(self) elif (self.count('Content-Type:') == 1 and 'application/json' in self): # Looks like a whole JSON HTTP message, # try to extract its body. try: j = self.strip()[self.strip().rindex('\r\n\r\n'):] except ValueError: pass else: try: self._json = json.loads(j) except ValueError: pass return self._json class ExitStatusError(Exception): pass def http(*args, **kwargs): # noinspection PyUnresolvedReferences """ Run HTTPie and capture stderr/out and exit status. Invoke `httpie.core.main()` with `args` and `kwargs`, and return a `CLIResponse` subclass instance. The return value is either a `StrCLIResponse`, or `BytesCLIResponse` if unable to decode the output. The response has the following attributes: `stdout` is represented by the instance itself (print r) `stderr`: text written to stderr `exit_status`: the exit status `json`: decoded JSON (if possible) or `None` Exceptions are propagated. If you pass ``error_exit_ok=True``, then error exit statuses won't result into an exception. Example: $ http --auth=user:password GET httpbin.org/basic-auth/user/password >>> httpbin = getfixture('httpbin') >>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw') >>> type(r) == StrCLIResponse True >>> r.exit_status 0 >>> r.stderr '' >>> 'HTTP/1.1 200 OK' in r True >>> r.json == {'authenticated': True, 'user': 'user'} True """ error_exit_ok = kwargs.pop('error_exit_ok', False) env = kwargs.get('env') if not env: env = kwargs['env'] = TestEnvironment() stdout = env.stdout stderr = env.stderr args = list(args) args_with_config_defaults = args + env.config.default_options add_to_args = [] if '--debug' not in args_with_config_defaults: if not error_exit_ok and '--traceback' not in args_with_config_defaults: add_to_args.append('--traceback') if not any('--timeout' in arg for arg in args_with_config_defaults): add_to_args.append('--timeout=3') args = add_to_args + args def dump_stderr(): stderr.seek(0) sys.stderr.write(stderr.read()) try: try: exit_status = main(args=args, **kwargs) if '--download' in args: # Let the progress reporter thread finish. time.sleep(.5) except SystemExit: if error_exit_ok: exit_status = ExitStatus.ERROR else: dump_stderr() raise except Exception: stderr.seek(0) sys.stderr.write(stderr.read()) raise else: if not error_exit_ok and exit_status != ExitStatus.OK: dump_stderr() raise ExitStatusError( 'httpie.core.main() unexpectedly returned' ' a non-zero exit status: {0} ({1})'.format( exit_status, EXIT_STATUS_LABELS[exit_status] ) ) stdout.seek(0) stderr.seek(0) output = stdout.read() try: output = output.decode('utf8') except UnicodeDecodeError: # noinspection PyArgumentList r = BytesCLIResponse(output) else: # noinspection PyArgumentList r = StrCLIResponse(output) r.stderr = stderr.read() r.exit_status = exit_status if r.exit_status != ExitStatus.OK: sys.stderr.write(r.stderr) return r finally: stdout.close() stderr.close() env.cleanup() httpie-0.9.8/tests/test_unicode.py0000644000000000000000000000557213022157774015745 0ustar rootroot# coding=utf-8 """ Various unicode handling related tests. """ from utils import http, HTTP_OK from fixtures import UNICODE def test_unicode_headers(httpbin): # httpbin doesn't interpret utf8 headers r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE) assert HTTP_OK in r def test_unicode_headers_verbose(httpbin): # httpbin doesn't interpret utf8 headers r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE) assert HTTP_OK in r assert UNICODE in r def test_unicode_form_item(httpbin): r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) assert HTTP_OK in r assert r.json['form'] == {'test': UNICODE} def test_unicode_form_item_verbose(httpbin): r = http('--verbose', '--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) assert HTTP_OK in r assert UNICODE in r def test_unicode_json_item(httpbin): r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) assert HTTP_OK in r assert r.json['json'] == {'test': UNICODE} def test_unicode_json_item_verbose(httpbin): r = http('--verbose', '--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) assert HTTP_OK in r assert UNICODE in r def test_unicode_raw_json_item(httpbin): r = http('--json', 'POST', httpbin.url + '/post', u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) assert HTTP_OK in r assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} def test_unicode_raw_json_item_verbose(httpbin): r = http('--json', 'POST', httpbin.url + '/post', u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) assert HTTP_OK in r assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} def test_unicode_url_query_arg_item(httpbin): r = http(httpbin.url + '/get', u'test==%s' % UNICODE) assert HTTP_OK in r assert r.json['args'] == {'test': UNICODE}, r def test_unicode_url_query_arg_item_verbose(httpbin): r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE) assert HTTP_OK in r assert UNICODE in r def test_unicode_url(httpbin): r = http(httpbin.url + u'/get?test=' + UNICODE) assert HTTP_OK in r assert r.json['args'] == {'test': UNICODE} # def test_unicode_url_verbose(self): # r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE) # assert HTTP_OK in r def test_unicode_basic_auth(httpbin): # it doesn't really authenticate us because httpbin # doesn't interpret the utf8-encoded auth http('--verbose', '--auth', u'test:%s' % UNICODE, httpbin.url + u'/basic-auth/test/' + UNICODE) def test_unicode_digest_auth(httpbin): # it doesn't really authenticate us because httpbin # doesn't interpret the utf8-encoded auth http('--auth-type=digest', '--auth', u'test:%s' % UNICODE, httpbin.url + u'/digest-auth/auth/test/' + UNICODE) httpie-0.9.8/tests/test_docs.py0000644000000000000000000000167413022157774015246 0ustar rootrootimport os import fnmatch import subprocess import pytest from utils import TESTS_ROOT def has_docutils(): try: # noinspection PyUnresolvedReferences import docutils return True except ImportError: return False def rst_filenames(): for root, dirnames, filenames in os.walk(os.path.dirname(TESTS_ROOT)): if '.tox' not in root: for filename in fnmatch.filter(filenames, '*.rst'): yield os.path.join(root, filename) filenames = list(rst_filenames()) assert filenames @pytest.mark.skipif(not has_docutils(), reason='docutils not installed') @pytest.mark.parametrize('filename', filenames) def test_rst_file_syntax(filename): p = subprocess.Popen( ['rst2pseudoxml.py', '--report=1', '--exit-status=1', filename], stderr=subprocess.PIPE, stdout=subprocess.PIPE ) err = p.communicate()[1] assert p.returncode == 0, err.decode('utf8') httpie-0.9.8/tests/test_redirects.py0000644000000000000000000000253313022157774016275 0ustar rootroot"""High-level tests.""" import pytest from httpie import ExitStatus from utils import http, HTTP_OK def test_follow_all_redirects_shown(httpbin): r = http('--follow', '--all', httpbin.url + '/redirect/2') assert r.count('HTTP/1.1') == 3 assert r.count('HTTP/1.1 302 FOUND', 2) assert HTTP_OK in r @pytest.mark.parametrize('follow_flag', ['--follow', '-F']) def test_follow_without_all_redirects_hidden(httpbin, follow_flag): r = http(follow_flag, httpbin.url + '/redirect/2') assert r.count('HTTP/1.1') == 1 assert HTTP_OK in r def test_follow_all_output_options_used_for_redirects(httpbin): r = http('--check-status', '--follow', '--all', '--print=H', httpbin.url + '/redirect/2') assert r.count('GET /') == 3 assert HTTP_OK not in r def test_follow_redirect_output_options(httpbin): r = http('--check-status', '--follow', '--all', '--print=h', '--history-print=H', httpbin.url + '/redirect/2') assert r.count('GET /') == 2 assert 'HTTP/1.1 302 FOUND' not in r assert HTTP_OK in r def test_max_redirects(httpbin): r = http('--max-redirects=1', '--follow', httpbin.url + '/redirect/3', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR_TOO_MANY_REDIRECTS httpie-0.9.8/tests/test_errors.py0000644000000000000000000000307513022157774015627 0ustar rootrootimport mock from pytest import raises from requests import Request, Timeout from requests.exceptions import ConnectionError from httpie import ExitStatus from httpie.core import main error_msg = None @mock.patch('httpie.core.get_response') def test_error(get_response): def error(msg, *args, **kwargs): global error_msg error_msg = msg % args exc = ConnectionError('Connection aborted') exc.request = Request(method='GET', url='http://www.google.com') get_response.side_effect = exc ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error) assert ret == ExitStatus.ERROR assert error_msg == ( 'ConnectionError: ' 'Connection aborted while doing GET request to URL: ' 'http://www.google.com') @mock.patch('httpie.core.get_response') def test_error_traceback(get_response): exc = ConnectionError('Connection aborted') exc.request = Request(method='GET', url='http://www.google.com') get_response.side_effect = exc with raises(ConnectionError): main(['--ignore-stdin', '--traceback', 'www.google.com']) @mock.patch('httpie.core.get_response') def test_timeout(get_response): def error(msg, *args, **kwargs): global error_msg error_msg = msg % args exc = Timeout('Request timed out') exc.request = Request(method='GET', url='http://www.google.com') get_response.side_effect = exc ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error) assert ret == ExitStatus.ERROR_TIMEOUT assert error_msg == 'Request timed out (30s).' httpie-0.9.8/tests/test_output.py0000644000000000000000000001377613022157774015664 0ustar rootrootimport os from tempfile import gettempdir import pytest from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF from httpie import ExitStatus from httpie.compat import urlopen from httpie.output.formatters.colors import get_lexer @pytest.mark.parametrize('stdout_isatty', [True, False]) def test_output_option(httpbin, stdout_isatty): output_filename = os.path.join(gettempdir(), test_output_option.__name__) url = httpbin + '/robots.txt' r = http('--output', output_filename, url, env=TestEnvironment(stdout_isatty=stdout_isatty)) assert r == '' expected_body = urlopen(url).read().decode() with open(output_filename, 'r') as f: actual_body = f.read() assert actual_body == expected_body class TestVerboseFlag: def test_verbose(self, httpbin): r = http('--verbose', 'GET', httpbin.url + '/get', 'test-header:__test__') assert HTTP_OK in r assert r.count('__test__') == 2 def test_verbose_form(self, httpbin): # https://github.com/jkbrzt/httpie/issues/53 r = http('--verbose', '--form', 'POST', httpbin.url + '/post', 'A=B', 'C=D') assert HTTP_OK in r assert 'A=B&C=D' in r def test_verbose_json(self, httpbin): r = http('--verbose', 'POST', httpbin.url + '/post', 'foo=bar', 'baz=bar') assert HTTP_OK in r assert '"baz": "bar"' in r def test_verbose_implies_all(self, httpbin): r = http('--verbose', '--follow', httpbin + '/redirect/1') assert 'GET /redirect/1 HTTP/1.1' in r assert 'HTTP/1.1 302 FOUND' in r assert 'GET /get HTTP/1.1' in r assert HTTP_OK in r class TestColors: @pytest.mark.parametrize( argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'], argvalues=[ ('application/json', False, None, 'JSON'), ('application/json+foo', False, None, 'JSON'), ('application/foo+json', False, None, 'JSON'), ('application/json-foo', False, None, 'JSON'), ('application/x-json', False, None, 'JSON'), ('foo/json', False, None, 'JSON'), ('foo/json+bar', False, None, 'JSON'), ('foo/bar+json', False, None, 'JSON'), ('foo/json-foo', False, None, 'JSON'), ('foo/x-json', False, None, 'JSON'), ('application/vnd.comverge.grid+hal+json', False, None, 'JSON'), ('text/plain', True, '{}', 'JSON'), ('text/plain', True, 'foo', 'Text only'), ] ) def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name): lexer = get_lexer(mime, body=body, explicit_json=explicit_json) assert lexer is not None assert lexer.name == expected_lexer_name def test_get_lexer_not_found(self): assert get_lexer('xxx/yyy') is None class TestPrettyOptions: """Test the --pretty flag handling.""" def test_pretty_enabled_by_default(self, httpbin): env = TestEnvironment(colors=256) r = http('GET', httpbin.url + '/get', env=env) assert COLOR in r def test_pretty_enabled_by_default_unless_stdout_redirected(self, httpbin): r = http('GET', httpbin.url + '/get') assert COLOR not in r def test_force_pretty(self, httpbin): env = TestEnvironment(stdout_isatty=False, colors=256) r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, ) assert COLOR in r def test_force_ugly(self, httpbin): r = http('--pretty=none', 'GET', httpbin.url + '/get') assert COLOR not in r def test_subtype_based_pygments_lexer_match(self, httpbin): """Test that media subtype is used if type/subtype doesn't match any lexer. """ env = TestEnvironment(colors=256) r = http('--print=B', '--pretty=all', httpbin.url + '/post', 'Content-Type:text/foo+json', 'a=b', env=env) assert COLOR in r def test_colors_option(self, httpbin): env = TestEnvironment(colors=256) r = http('--print=B', '--pretty=colors', 'GET', httpbin.url + '/get', 'a=b', env=env) # Tests that the JSON data isn't formatted. assert not r.strip().count('\n') assert COLOR in r def test_format_option(self, httpbin): env = TestEnvironment(colors=256) r = http('--print=B', '--pretty=format', 'GET', httpbin.url + '/get', 'a=b', env=env) # Tests that the JSON data is formatted. assert r.strip().count('\n') == 2 assert COLOR not in r class TestLineEndings: """ Test that CRLF is properly used in headers and as the headers/body separator. """ def _validate_crlf(self, msg): lines = iter(msg.splitlines(True)) for header in lines: if header == CRLF: break assert header.endswith(CRLF), repr(header) else: assert 0, 'CRLF between headers and body not found in %r' % msg body = ''.join(lines) assert CRLF not in body return body def test_CRLF_headers_only(self, httpbin): r = http('--headers', 'GET', httpbin.url + '/get') body = self._validate_crlf(r) assert not body, 'Garbage after headers: %r' % r def test_CRLF_ugly_response(self, httpbin): r = http('--pretty=none', 'GET', httpbin.url + '/get') self._validate_crlf(r) def test_CRLF_formatted_response(self, httpbin): r = http('--pretty=format', 'GET', httpbin.url + '/get') assert r.exit_status == ExitStatus.OK self._validate_crlf(r) def test_CRLF_ugly_request(self, httpbin): r = http('--pretty=none', '--print=HB', 'GET', httpbin.url + '/get') self._validate_crlf(r) def test_CRLF_formatted_request(self, httpbin): r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get') self._validate_crlf(r) httpie-0.9.8/tests/test_uploads.py0000644000000000000000000000556313022157774015766 0ustar rootrootimport os import pytest from httpie.input import ParseError from utils import TestEnvironment, http, HTTP_OK from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT class TestMultipartFormDataFileUpload: def test_non_existent_file_raises_parse_error(self, httpbin): with pytest.raises(ParseError): http('--form', 'POST', httpbin.url + '/post', 'foo@/__does_not_exist__') def test_upload_ok(self, httpbin): r = http('--form', '--verbose', 'POST', httpbin.url + '/post', 'test-file@%s' % FILE_PATH_ARG, 'foo=bar') assert HTTP_OK in r assert 'Content-Disposition: form-data; name="foo"' in r assert 'Content-Disposition: form-data; name="test-file";' \ ' filename="%s"' % os.path.basename(FILE_PATH) in r assert FILE_CONTENT in r assert '"foo": "bar"' in r assert 'Content-Type: text/plain' in r def test_upload_multiple_fields_with_the_same_name(self, httpbin): r = http('--form', '--verbose', 'POST', httpbin.url + '/post', 'test-file@%s' % FILE_PATH_ARG, 'test-file@%s' % FILE_PATH_ARG) assert HTTP_OK in r assert r.count('Content-Disposition: form-data; name="test-file";' ' filename="%s"' % os.path.basename(FILE_PATH)) == 2 # Should be 4, but is 3 because httpbin # doesn't seem to support filed field lists assert r.count(FILE_CONTENT) in [3, 4] assert r.count('Content-Type: text/plain') == 2 class TestRequestBodyFromFilePath: """ `http URL @file' """ def test_request_body_from_file_by_path(self, httpbin): r = http('--verbose', 'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG) assert HTTP_OK in r assert FILE_CONTENT in r, r assert '"Content-Type": "text/plain"' in r def test_request_body_from_file_by_path_with_explicit_content_type( self, httpbin): r = http('--verbose', 'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'Content-Type:text/plain; charset=utf8') assert HTTP_OK in r assert FILE_CONTENT in r assert 'Content-Type: text/plain; charset=utf8' in r def test_request_body_from_file_by_path_no_field_name_allowed( self, httpbin): env = TestEnvironment(stdin_isatty=True) r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG, env=env, error_exit_ok=True) assert 'perhaps you meant --form?' in r.stderr def test_request_body_from_file_by_path_no_data_items_allowed( self, httpbin): env = TestEnvironment(stdin_isatty=False) r = http('POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'foo=bar', env=env, error_exit_ok=True) assert 'cannot be mixed' in r.stderr httpie-0.9.8/tests/test_downloads.py0000644000000000000000000001334113022157774016302 0ustar rootrootimport os import time import pytest import mock from requests.structures import CaseInsensitiveDict from httpie.compat import urlopen from httpie.downloads import ( parse_content_range, filename_from_content_disposition, filename_from_url, get_unique_filename, ContentRangeError, Downloader, ) from utils import http, TestEnvironment class Response(object): # noinspection PyDefaultArgument def __init__(self, url, headers={}, status_code=200): self.url = url self.headers = CaseInsensitiveDict(headers) self.status_code = status_code class TestDownloadUtils: def test_Content_Range_parsing(self): parse = parse_content_range assert parse('bytes 100-199/200', 100) == 200 assert parse('bytes 100-199/*', 100) == 200 # missing pytest.raises(ContentRangeError, parse, None, 100) # syntax error pytest.raises(ContentRangeError, parse, 'beers 100-199/*', 100) # unexpected range pytest.raises(ContentRangeError, parse, 'bytes 100-199/*', 99) # invalid instance-length pytest.raises(ContentRangeError, parse, 'bytes 100-199/199', 100) # invalid byte-range-resp-spec pytest.raises(ContentRangeError, parse, 'bytes 100-99/199', 100) # invalid byte-range-resp-spec pytest.raises(ContentRangeError, parse, 'bytes 100-100/*', 100) @pytest.mark.parametrize('header, expected_filename', [ ('attachment; filename=hello-WORLD_123.txt', 'hello-WORLD_123.txt'), ('attachment; filename=".hello-WORLD_123.txt"', 'hello-WORLD_123.txt'), ('attachment; filename="white space.txt"', 'white space.txt'), (r'attachment; filename="\"quotes\".txt"', '"quotes".txt'), ('attachment; filename=/etc/hosts', 'hosts'), ('attachment; filename=', None) ]) def test_Content_Disposition_parsing(self, header, expected_filename): assert filename_from_content_disposition(header) == expected_filename def test_filename_from_url(self): assert 'foo.txt' == filename_from_url( url='http://example.org/foo', content_type='text/plain' ) assert 'foo.html' == filename_from_url( url='http://example.org/foo', content_type='text/html; charset=utf8' ) assert 'foo' == filename_from_url( url='http://example.org/foo', content_type=None ) assert 'foo' == filename_from_url( url='http://example.org/foo', content_type='x-foo/bar' ) @pytest.mark.parametrize( 'orig_name, unique_on_attempt, expected', [ # Simple ('foo.bar', 0, 'foo.bar'), ('foo.bar', 1, 'foo.bar-1'), ('foo.bar', 10, 'foo.bar-10'), # Trim ('A' * 20, 0, 'A' * 10), ('A' * 20, 1, 'A' * 8 + '-1'), ('A' * 20, 10, 'A' * 7 + '-10'), # Trim before ext ('A' * 20 + '.txt', 0, 'A' * 6 + '.txt'), ('A' * 20 + '.txt', 1, 'A' * 4 + '.txt-1'), # Trim at the end ('foo.' + 'A' * 20, 0, 'foo.' + 'A' * 6), ('foo.' + 'A' * 20, 1, 'foo.' + 'A' * 4 + '-1'), ('foo.' + 'A' * 20, 10, 'foo.' + 'A' * 3 + '-10'), ] ) @mock.patch('httpie.downloads.get_filename_max_length') def test_unique_filename(self, get_filename_max_length, orig_name, unique_on_attempt, expected): def attempts(unique_on_attempt=0): # noinspection PyUnresolvedReferences,PyUnusedLocal def exists(filename): if exists.attempt == unique_on_attempt: return False exists.attempt += 1 return True exists.attempt = 0 return exists get_filename_max_length.return_value = 10 actual = get_unique_filename(orig_name, attempts(unique_on_attempt)) assert expected == actual class TestDownloads: # TODO: more tests def test_actual_download(self, httpbin_both, httpbin): robots_txt = '/robots.txt' body = urlopen(httpbin + robots_txt).read().decode() env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('--download', httpbin_both.url + robots_txt, env=env) assert 'Downloading' in r.stderr assert '[K' in r.stderr assert 'Done' in r.stderr assert body == r def test_download_with_Content_Length(self, httpbin_both): devnull = open(os.devnull, 'w') downloader = Downloader(output_file=devnull, progress_file=devnull) downloader.start(Response( url=httpbin_both.url + '/', headers={'Content-Length': 10} )) time.sleep(1.1) downloader.chunk_downloaded(b'12345') time.sleep(1.1) downloader.chunk_downloaded(b'12345') downloader.finish() assert not downloader.interrupted def test_download_no_Content_Length(self, httpbin_both): devnull = open(os.devnull, 'w') downloader = Downloader(output_file=devnull, progress_file=devnull) downloader.start(Response(url=httpbin_both.url + '/')) time.sleep(1.1) downloader.chunk_downloaded(b'12345') downloader.finish() assert not downloader.interrupted def test_download_interrupted(self, httpbin_both): devnull = open(os.devnull, 'w') downloader = Downloader(output_file=devnull, progress_file=devnull) downloader.start(Response( url=httpbin_both.url + '/', headers={'Content-Length': 5} )) downloader.chunk_downloaded(b'1234') downloader.finish() assert downloader.interrupted httpie-0.9.8/tests/test_ssl.py0000644000000000000000000000605613022157774015116 0ustar rootrootimport os import pytest import pytest_httpbin.certs from requests.exceptions import SSLError from httpie import ExitStatus from httpie.input import SSL_VERSION_ARG_MAPPING from utils import http, HTTP_OK, TESTS_ROOT CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt') CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key') CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem') # FIXME: # We test against a local httpbin instance which uses a self-signed cert. # Requests without --verify= will fail with a verification error. # See: https://github.com/kevin1024/pytest-httpbin#https-support CA_BUNDLE = pytest_httpbin.certs.where() @pytest.mark.parametrize('ssl_version', SSL_VERSION_ARG_MAPPING.keys()) def test_ssl_version(httpbin_secure, ssl_version): try: r = http( '--ssl', ssl_version, httpbin_secure + '/get' ) assert HTTP_OK in r except SSLError as e: if ssl_version == 'ssl3': # pytest-httpbin doesn't support ssl3 assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e) else: raise class TestClientCert: def test_cert_and_key(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', CLIENT_CERT, '--cert-key', CLIENT_KEY) assert HTTP_OK in r def test_cert_pem(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', CLIENT_PEM) assert HTTP_OK in r def test_cert_file_not_found(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', '/__not_found__', error_exit_ok=True) assert r.exit_status == ExitStatus.ERROR assert 'No such file or directory' in r.stderr def test_cert_file_invalid(self, httpbin_secure): with pytest.raises(SSLError): http(httpbin_secure + '/get', '--cert', __file__) def test_cert_ok_but_missing_key(self, httpbin_secure): with pytest.raises(SSLError): http(httpbin_secure + '/get', '--cert', CLIENT_CERT) class TestServerCert: def test_verify_no_OK(self, httpbin_secure): r = http(httpbin_secure.url + '/get', '--verify=no') assert HTTP_OK in r def test_verify_custom_ca_bundle_path( self, httpbin_secure_untrusted): r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE) assert HTTP_OK in r def test_self_signed_server_cert_by_default_raises_ssl_error( self, httpbin_secure_untrusted): with pytest.raises(SSLError): http(httpbin_secure_untrusted.url + '/get') def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure): with pytest.raises(SSLError): http(httpbin_secure.url + '/get', '--verify', '/__not_found__') def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure): with pytest.raises(SSLError): http(httpbin_secure.url + '/get', '--verify', __file__) httpie-0.9.8/tests/test_httpie.py0000644000000000000000000000667513022157774015621 0ustar rootroot"""High-level tests.""" import pytest from httpie.input import ParseError from utils import TestEnvironment, http, HTTP_OK from fixtures import FILE_PATH, FILE_CONTENT import httpie from httpie.compat import is_py26 def test_debug(): r = http('--debug') assert r.exit_status == httpie.ExitStatus.OK assert 'HTTPie %s' % httpie.__version__ in r.stderr def test_help(): r = http('--help', error_exit_ok=True) assert r.exit_status == httpie.ExitStatus.OK assert 'https://github.com/jkbrzt/httpie/issues' in r def test_version(): r = http('--version', error_exit_ok=True) assert r.exit_status == httpie.ExitStatus.OK # FIXME: py3 has version in stdout, py2 in stderr assert httpie.__version__ == r.stderr.strip() + r.strip() def test_GET(httpbin_both): r = http('GET', httpbin_both + '/get') assert HTTP_OK in r def test_DELETE(httpbin_both): r = http('DELETE', httpbin_both + '/delete') assert HTTP_OK in r def test_PUT(httpbin_both): r = http('PUT', httpbin_both + '/put', 'foo=bar') assert HTTP_OK in r assert r.json['json']['foo'] == 'bar' def test_POST_JSON_data(httpbin_both): r = http('POST', httpbin_both + '/post', 'foo=bar') assert HTTP_OK in r assert r.json['json']['foo'] == 'bar' def test_POST_form(httpbin_both): r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar') assert HTTP_OK in r assert '"foo": "bar"' in r def test_POST_form_multiple_values(httpbin_both): r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar', 'foo=baz') assert HTTP_OK in r assert r.json['form'] == {'foo': ['bar', 'baz']} def test_POST_stdin(httpbin_both): with open(FILE_PATH) as f: env = TestEnvironment(stdin=f, stdin_isatty=False) r = http('--form', 'POST', httpbin_both + '/post', env=env) assert HTTP_OK in r assert FILE_CONTENT in r def test_headers(httpbin_both): r = http('GET', httpbin_both + '/headers', 'Foo:bar') assert HTTP_OK in r assert '"User-Agent": "HTTPie' in r, r assert '"Foo": "bar"' in r def test_headers_unset(httpbin_both): r = http('GET', httpbin_both + '/headers') assert 'Accept' in r.json['headers'] # default Accept present r = http('GET', httpbin_both + '/headers', 'Accept:') assert 'Accept' not in r.json['headers'] # default Accept unset @pytest.mark.skip('unimplemented') def test_unset_host_header(httpbin_both): r = http('GET', httpbin_both + '/headers') assert 'Host' in r.json['headers'] # default Host present r = http('GET', httpbin_both + '/headers', 'Host:') assert 'Host' not in r.json['headers'] # default Host unset def test_headers_empty_value(httpbin_both): r = http('GET', httpbin_both + '/headers') assert r.json['headers']['Accept'] # default Accept has value r = http('GET', httpbin_both + '/headers', 'Accept;') assert r.json['headers']['Accept'] == '' # Accept has no value def test_headers_empty_value_with_value_gives_error(httpbin): with pytest.raises(ParseError): http('GET', httpbin + '/headers', 'Accept;SYNTAX_ERROR') @pytest.mark.skipif( is_py26, reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only' ) def test_json_input_preserve_order(httpbin_both): r = http('PATCH', httpbin_both + '/patch', 'order:={"map":{"1":"first","2":"second"}}') assert HTTP_OK in r assert r.json['data'] == \ '{"order": {"map": {"1": "first", "2": "second"}}}' httpie-0.9.8/tests/test_config.py0000644000000000000000000000170413022157774015555 0ustar rootrootfrom utils import TestEnvironment, http def test_default_options(httpbin): env = TestEnvironment() env.config['default_options'] = ['--form'] env.config.save() r = http(httpbin.url + '/post', 'foo=bar', env=env) assert r.json['form'] == {"foo": "bar"} def test_default_options_overwrite(httpbin): env = TestEnvironment() env.config['default_options'] = ['--form'] env.config.save() r = http('--json', httpbin.url + '/post', 'foo=bar', env=env) assert r.json['json'] == {"foo": "bar"} def test_migrate_implicit_content_type(): config = TestEnvironment().config config['implicit_content_type'] = 'json' config.save() config.load() assert 'implicit_content_type' not in config assert not config['default_options'] config['implicit_content_type'] = 'form' config.save() config.load() assert 'implicit_content_type' not in config assert config['default_options'] == ['--form'] httpie-0.9.8/tests/test_auth.py0000644000000000000000000000444613022157774015257 0ustar rootroot"""HTTP authentication-related tests.""" import mock import pytest from utils import http, add_auth, HTTP_OK, TestEnvironment import httpie.input import httpie.cli def test_basic_auth(httpbin_both): r = http('--auth=user:password', 'GET', httpbin_both + '/basic-auth/user/password') assert HTTP_OK in r assert r.json == {'authenticated': True, 'user': 'user'} @pytest.mark.parametrize('argument_name', ['--auth-type', '-A']) def test_digest_auth(httpbin_both, argument_name): r = http(argument_name + '=digest', '--auth=user:password', 'GET', httpbin_both.url + '/digest-auth/auth/user/password') assert HTTP_OK in r assert r.json == {'authenticated': True, 'user': 'user'} @mock.patch('httpie.input.AuthCredentials._getpass', new=lambda self, prompt: 'password') def test_password_prompt(httpbin): r = http('--auth', 'user', 'GET', httpbin.url + '/basic-auth/user/password') assert HTTP_OK in r assert r.json == {'authenticated': True, 'user': 'user'} def test_credentials_in_url(httpbin_both): url = add_auth(httpbin_both.url + '/basic-auth/user/password', auth='user:password') r = http('GET', url) assert HTTP_OK in r assert r.json == {'authenticated': True, 'user': 'user'} def test_credentials_in_url_auth_flag_has_priority(httpbin_both): """When credentials are passed in URL and via -a at the same time, then the ones from -a are used.""" url = add_auth(httpbin_both.url + '/basic-auth/user/password', auth='user:wrong') r = http('--auth=user:password', 'GET', url) assert HTTP_OK in r assert r.json == {'authenticated': True, 'user': 'user'} @pytest.mark.parametrize('url', [ 'username@example.org', 'username:@example.org', ]) def test_only_username_in_url(url): """ https://github.com/jkbrzt/httpie/issues/242 """ args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment()) assert args.auth assert args.auth.username == 'username' assert args.auth.password == '' def test_missing_auth(httpbin): r = http( '--auth-type=basic', 'GET', httpbin + '/basic-auth/user/password', error_exit_ok=True ) assert HTTP_OK not in r assert '--auth required' in r.stderr httpie-0.9.8/tests/client_certs/0000755000000000000000000000000013022157774015353 5ustar rootroothttpie-0.9.8/tests/client_certs/client.crt0000644000000000000000000000340513022157774017345 0ustar rootroot-----BEGIN CERTIFICATE----- MIIFAjCCAuoCAQEwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCVVMxCzAJBgNV BAgTAkNBMQswCQYDVQQHEwJTRjEPMA0GA1UEChMGSFRUUGllMQ8wDQYDVQQDEwZI VFRQaWUwHhcNMTUwMTIzMjIyNTM2WhcNMTYwMTIzMjIyNTM2WjBFMQswCQYDVQQG EwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lk Z2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu6aP iR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/Rn5mCMKmD506JrFV8fktQ M6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535lb9V9hHjAgy60QgJBgSE7 lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et0RQiWIi7S6vpDRpZFxRi gtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQI6JadczU0JyVVjJVTny3 ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ2nc+OrJwYLvOp1cG/zYl GHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK3gEbMz3y+YTlVNPo108H JI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZdVH3feAhTfDZbpSxhpRo Ja84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQFTCjN22UhPP0PrqY3ngEj 1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5Vr89NO08QtnLwQduusVkc 4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0EyV2z6pZiH6HK1r5Xwaq 0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEAATANBgkqhkiG9w0BAQUF AAOCAgEAQgIicN/uWtaYYBVEVeMGMdxzpp2pv3AaCfQMoVGaQu9VLydK/GBlYOqj AGPjdmQ7p4ISlduXqslu646+RxZ+H6TSSj0NTF4FyR8LPckRPiePNlsGp3u6ffix PX0554Ks+JYyFJ7qyMhsilqCYtw8prX9lj8fjzbWWXlgJFH/SRZw4xdcJ1yYA9sQ fBHxveCWFS1ibX5+QGy/+7jPb99MP38HEIt9vTMW5aiwXeIbipXohWqcJhxL9GXz KPsrt9a++rLjqsquhZL4uCksGmI4Gv0FQQswgSyHSSQzagee5VRB68WYSAyYdvzi YCfkNcbQtOOQWGx4rsEdENViPs1GEZkWJJ1h9pmWzZl0U9c3cnABffK7o9v6ap2F NrnU5H/7jLuBiUJFzqwkgAjANLRZ6hLj6h/grcnIIThJwg6KaXvpEh4UkHuqHYBF Fq1BWZIWU25ASggEVIsCPXC2+I1oGhxK1DN/J+wIht9MBWWlQWVMZAQsBkszNZrh nzdfMoQZTG5bT4Bf0bI5LmPaY0xBxXA1f4TLuqrEAziOjRX3vIQV4i33nZZJvPcC mCoyhAUpTJm+OI90ePll+vBO1ENAx7EMHqNe6eCChZ/9DUsVxxtaorVq1l0xWons ynOCgx46hGE12/oiRIKq/wGMpv6ClfJhW1N5nJahDqoIMEvnNaQ= -----END CERTIFICATE----- httpie-0.9.8/tests/client_certs/client.key0000644000000000000000000000625313022157774017351 0ustar rootroot-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAu6aPiR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/R n5mCMKmD506JrFV8fktQM6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535l b9V9hHjAgy60QgJBgSE7lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et 0RQiWIi7S6vpDRpZFxRigtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQ I6JadczU0JyVVjJVTny3ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ 2nc+OrJwYLvOp1cG/zYlGHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK 3gEbMz3y+YTlVNPo108HJI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZ dVH3feAhTfDZbpSxhpRoJa84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQF TCjN22UhPP0PrqY3ngEj1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5V r89NO08QtnLwQduusVkc4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0 EyV2z6pZiH6HK1r5Xwaq0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEA AQKCAgBOY1DYlZYg8/eXAhuDDkayYYzDuny1ylG8c4F9nFYVCxB2GZ1Wz3icPWP1 j1BhpkBgPbPeLfM+O0V1H6eCdVvapKOxXM52mDuHO3TJP6P8lOZgZOOY6RUK7qp0 4mC4plqYx7oto23CBLoOdgMtM937rG0SLGDfIF6z8sI0XCMRkqPpRviNu5xxYYTk IoczSwtmYcSZJRjHhk4AGnmicDbMPRlJ2k2E0euHhI9wMAyQFUFnhLJlQGALj6pj DtYvcM1EAUN46EXK66bXQq8zgozYS0WIJ6+wOUKQMSIgUGCF6Rvm3ZTt9xwOxxW8 wxebvfYVTJgIdh2Nfusgmye9Debl73f+k9/O4RsvYc5J5w2n4IxKqQrfCZrZqevZ s+KvARkuQbXrHPanvEd8MPrRZ6FOAdiZYAbB9OvzuKCbEkgag8GPjMMAvrjT49N2 qp9gwGgnzczQYn+vLblJuRzofcblvLE+sxKKDE8qrfcOjN1murZP7714y5E3NmEZ NB2NTHveTflYI1HJ1tznI1C40GdBYH4GwT/0he53rBcjNaPhyP7j3cTR1doRfZap 2oz8KE/Sij3Zb6b8r7hi+Lcwpa9txZftro7XNOJIX7ZT5B4KMiXowtCHbkMMnL6k 48tRBpyX20MqDFezBRCK7lfGhU1Coms8UcDHoFXLuGY/sAYEcQKCAQEA9D9/PD1t e90haG6nLl4LKP5wH2wB2BK1RRBERqOVqwSmdSgn3/+GkpeYWKdhN2jyYn6qnpJQ hXXYGtHAAHuw0dGXnOmgcsyZSlAWPzpMYRYrSh3ds8JVJdV2d58yS0ty3Ae3W6aW p4SRuhf8yIMgOmE+TARCU1rJdke9jIIl2TQmnpJahlsZeGLEmEXE99EhB5VoshRJ hLXNn3xTtkQz3tNR0rMAtXI6SIMB00FFEG1+jClza6PYriT9dkORI5LSVqXDEpxR C41PvYMKTAloWd0hZ2gdfwAcJScoAv75L10uR7O1IeQI+Et5h2tj4a/OfzILa0d5 BYMmVsTa3NZXLQKCAQEAxK3uJKmoN2uswJQSKpX4WziVBgbaZdpCNnAWhfJZYIcP zlIuv9qOc/DIPiy9Sa9XtITSkcONAwRowtI783AtXeAelyo3W7A2aLIfBBZAXDzJ 8KMc9xMDPebvUhlPSzg4bNwvduukAnktlzCjrRWPXRoSfloSpFkFPP4GwTdVcf17 1mkki6rK4rbHmIoCITlZkNbUBCcu20ivK6N3pvH1wN123bxnF7lwvB5qizdFO5P7 xRVIoCdCXQ0+WK2ZokCa/r44rcp4ffgrLoO/WRlo4yERIa9NwaucIrXmotKX8kYc YYpFzrGs72DljS7TBZCOqek5fNQBNK79BA2hNcJ1FQKCAQBC+M44hFdq6T1p1z18 F0lUGkBAPWtcBfUyVL2D6QL2+7Vw1mvonbYWp/6cAHlFqj8cBsNd65ysm51/7ReK il/3iFLcMatPDw7RM5iGCcQ7ssp37iyGR7j1QMzVDA/MWYnLD0qVlN4mXNFgh4dG q73AhD2CtoBBPtmS1yUATAd4wTX9sP+la4FWYy6o2iiiEvPNkog8nBd0ji0tl/eU OKtIZAVBkteU6RdWHqX3eSQo1v0mDY+aajjVt0rQjMJVUMLgA1+z0KzgUAUXX8EJ DGNSkLHCGuhLlIojHdN4ztUgyZoRCxOVkWNsQbW3Dhk7HuuuMNi0t8pVWpq+nAev Gg6ZAoIBAQC0mMk9nRO7oAGG6/Aqbn8YtEISwKQ2Nk3qUs47vKdZPWvEFi6bOILp 70TP4qEFUh6EwhngguGuzZOsoQMvq+fcdXlhcQBYDtxHEpfsVspOZ/s+HWjxbuHh K3bBuj/XYA5f12c2GXYGV2MHm0AQJOX5pYEpyGepxZxLvy5QqRCqlQnrfaxzGycl OpTYepEuFM0rdDhGf/xEmt9OgNHT2AXDTRhizycS39Kmyn8myl+mL2JWPA7uEF6d txVytCWImS45kE3XNz2g3go4sf04QV7QgIKMnb4Wgg/ix4i6JgokC0DwR9mFzBxx ylW+aCqYx35YgrGo77sTt0LZP/KxvJdpAoIBAF7YfhR1wFbW2L8sJ4gAbjPUWOMu JUfE4FhdLcSdqCo+N8uN0qawJxXltBKfjeeoH0CDh9Yv0qqalVdSOKS9BPAa1zJc o2kBcT8AVwoPS5oxa9eDT+7iHPMF4BErB2IGv3yYwpjqSZBJ9TsTu1B6iTf5hOL5 9pqcv/LjfcwtWu2XMCVoZj2Q8iYv55l3jJ1ByF/UDVezWajE69avvJkQZrMZmuBw UuHelP/7anRyyelh7RkndZpPCExGmuO7pd5aG25/mBs0i34R1PElAtt8AN36f5Tk 1GxIltTNtLk4Mivwp9aZ1vf9s5FAhgPDvfGV5yFoKYmA/65ZlrKx0zlFNng= -----END RSA PRIVATE KEY----- httpie-0.9.8/tests/client_certs/client.pem0000644000000000000000000001235213022157774017337 0ustar rootrootBag Attributes localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9 subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd issuer=/C=US/ST=CA/L=SF/O=HTTPie/CN=HTTPie -----BEGIN CERTIFICATE----- MIIFAjCCAuoCAQEwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCVVMxCzAJBgNV BAgTAkNBMQswCQYDVQQHEwJTRjEPMA0GA1UEChMGSFRUUGllMQ8wDQYDVQQDEwZI VFRQaWUwHhcNMTUwMTIzMjIyNTM2WhcNMTYwMTIzMjIyNTM2WjBFMQswCQYDVQQG EwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lk Z2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu6aP iR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/Rn5mCMKmD506JrFV8fktQ M6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535lb9V9hHjAgy60QgJBgSE7 lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et0RQiWIi7S6vpDRpZFxRi gtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQI6JadczU0JyVVjJVTny3 ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ2nc+OrJwYLvOp1cG/zYl GHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK3gEbMz3y+YTlVNPo108H JI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZdVH3feAhTfDZbpSxhpRo Ja84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQFTCjN22UhPP0PrqY3ngEj 1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5Vr89NO08QtnLwQduusVkc 4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0EyV2z6pZiH6HK1r5Xwaq 0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEAATANBgkqhkiG9w0BAQUF AAOCAgEAQgIicN/uWtaYYBVEVeMGMdxzpp2pv3AaCfQMoVGaQu9VLydK/GBlYOqj AGPjdmQ7p4ISlduXqslu646+RxZ+H6TSSj0NTF4FyR8LPckRPiePNlsGp3u6ffix PX0554Ks+JYyFJ7qyMhsilqCYtw8prX9lj8fjzbWWXlgJFH/SRZw4xdcJ1yYA9sQ fBHxveCWFS1ibX5+QGy/+7jPb99MP38HEIt9vTMW5aiwXeIbipXohWqcJhxL9GXz KPsrt9a++rLjqsquhZL4uCksGmI4Gv0FQQswgSyHSSQzagee5VRB68WYSAyYdvzi YCfkNcbQtOOQWGx4rsEdENViPs1GEZkWJJ1h9pmWzZl0U9c3cnABffK7o9v6ap2F NrnU5H/7jLuBiUJFzqwkgAjANLRZ6hLj6h/grcnIIThJwg6KaXvpEh4UkHuqHYBF Fq1BWZIWU25ASggEVIsCPXC2+I1oGhxK1DN/J+wIht9MBWWlQWVMZAQsBkszNZrh nzdfMoQZTG5bT4Bf0bI5LmPaY0xBxXA1f4TLuqrEAziOjRX3vIQV4i33nZZJvPcC mCoyhAUpTJm+OI90ePll+vBO1ENAx7EMHqNe6eCChZ/9DUsVxxtaorVq1l0xWons ynOCgx46hGE12/oiRIKq/wGMpv6ClfJhW1N5nJahDqoIMEvnNaQ= -----END CERTIFICATE----- Bag Attributes localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9 Key Attributes: -----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAu6aPiR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/R n5mCMKmD506JrFV8fktQM6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535l b9V9hHjAgy60QgJBgSE7lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et 0RQiWIi7S6vpDRpZFxRigtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQ I6JadczU0JyVVjJVTny3ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ 2nc+OrJwYLvOp1cG/zYlGHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK 3gEbMz3y+YTlVNPo108HJI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZ dVH3feAhTfDZbpSxhpRoJa84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQF TCjN22UhPP0PrqY3ngEj1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5V r89NO08QtnLwQduusVkc4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0 EyV2z6pZiH6HK1r5Xwaq0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEA AQKCAgBOY1DYlZYg8/eXAhuDDkayYYzDuny1ylG8c4F9nFYVCxB2GZ1Wz3icPWP1 j1BhpkBgPbPeLfM+O0V1H6eCdVvapKOxXM52mDuHO3TJP6P8lOZgZOOY6RUK7qp0 4mC4plqYx7oto23CBLoOdgMtM937rG0SLGDfIF6z8sI0XCMRkqPpRviNu5xxYYTk IoczSwtmYcSZJRjHhk4AGnmicDbMPRlJ2k2E0euHhI9wMAyQFUFnhLJlQGALj6pj DtYvcM1EAUN46EXK66bXQq8zgozYS0WIJ6+wOUKQMSIgUGCF6Rvm3ZTt9xwOxxW8 wxebvfYVTJgIdh2Nfusgmye9Debl73f+k9/O4RsvYc5J5w2n4IxKqQrfCZrZqevZ s+KvARkuQbXrHPanvEd8MPrRZ6FOAdiZYAbB9OvzuKCbEkgag8GPjMMAvrjT49N2 qp9gwGgnzczQYn+vLblJuRzofcblvLE+sxKKDE8qrfcOjN1murZP7714y5E3NmEZ NB2NTHveTflYI1HJ1tznI1C40GdBYH4GwT/0he53rBcjNaPhyP7j3cTR1doRfZap 2oz8KE/Sij3Zb6b8r7hi+Lcwpa9txZftro7XNOJIX7ZT5B4KMiXowtCHbkMMnL6k 48tRBpyX20MqDFezBRCK7lfGhU1Coms8UcDHoFXLuGY/sAYEcQKCAQEA9D9/PD1t e90haG6nLl4LKP5wH2wB2BK1RRBERqOVqwSmdSgn3/+GkpeYWKdhN2jyYn6qnpJQ hXXYGtHAAHuw0dGXnOmgcsyZSlAWPzpMYRYrSh3ds8JVJdV2d58yS0ty3Ae3W6aW p4SRuhf8yIMgOmE+TARCU1rJdke9jIIl2TQmnpJahlsZeGLEmEXE99EhB5VoshRJ hLXNn3xTtkQz3tNR0rMAtXI6SIMB00FFEG1+jClza6PYriT9dkORI5LSVqXDEpxR C41PvYMKTAloWd0hZ2gdfwAcJScoAv75L10uR7O1IeQI+Et5h2tj4a/OfzILa0d5 BYMmVsTa3NZXLQKCAQEAxK3uJKmoN2uswJQSKpX4WziVBgbaZdpCNnAWhfJZYIcP zlIuv9qOc/DIPiy9Sa9XtITSkcONAwRowtI783AtXeAelyo3W7A2aLIfBBZAXDzJ 8KMc9xMDPebvUhlPSzg4bNwvduukAnktlzCjrRWPXRoSfloSpFkFPP4GwTdVcf17 1mkki6rK4rbHmIoCITlZkNbUBCcu20ivK6N3pvH1wN123bxnF7lwvB5qizdFO5P7 xRVIoCdCXQ0+WK2ZokCa/r44rcp4ffgrLoO/WRlo4yERIa9NwaucIrXmotKX8kYc YYpFzrGs72DljS7TBZCOqek5fNQBNK79BA2hNcJ1FQKCAQBC+M44hFdq6T1p1z18 F0lUGkBAPWtcBfUyVL2D6QL2+7Vw1mvonbYWp/6cAHlFqj8cBsNd65ysm51/7ReK il/3iFLcMatPDw7RM5iGCcQ7ssp37iyGR7j1QMzVDA/MWYnLD0qVlN4mXNFgh4dG q73AhD2CtoBBPtmS1yUATAd4wTX9sP+la4FWYy6o2iiiEvPNkog8nBd0ji0tl/eU OKtIZAVBkteU6RdWHqX3eSQo1v0mDY+aajjVt0rQjMJVUMLgA1+z0KzgUAUXX8EJ DGNSkLHCGuhLlIojHdN4ztUgyZoRCxOVkWNsQbW3Dhk7HuuuMNi0t8pVWpq+nAev Gg6ZAoIBAQC0mMk9nRO7oAGG6/Aqbn8YtEISwKQ2Nk3qUs47vKdZPWvEFi6bOILp 70TP4qEFUh6EwhngguGuzZOsoQMvq+fcdXlhcQBYDtxHEpfsVspOZ/s+HWjxbuHh K3bBuj/XYA5f12c2GXYGV2MHm0AQJOX5pYEpyGepxZxLvy5QqRCqlQnrfaxzGycl OpTYepEuFM0rdDhGf/xEmt9OgNHT2AXDTRhizycS39Kmyn8myl+mL2JWPA7uEF6d txVytCWImS45kE3XNz2g3go4sf04QV7QgIKMnb4Wgg/ix4i6JgokC0DwR9mFzBxx ylW+aCqYx35YgrGo77sTt0LZP/KxvJdpAoIBAF7YfhR1wFbW2L8sJ4gAbjPUWOMu JUfE4FhdLcSdqCo+N8uN0qawJxXltBKfjeeoH0CDh9Yv0qqalVdSOKS9BPAa1zJc o2kBcT8AVwoPS5oxa9eDT+7iHPMF4BErB2IGv3yYwpjqSZBJ9TsTu1B6iTf5hOL5 9pqcv/LjfcwtWu2XMCVoZj2Q8iYv55l3jJ1ByF/UDVezWajE69avvJkQZrMZmuBw UuHelP/7anRyyelh7RkndZpPCExGmuO7pd5aG25/mBs0i34R1PElAtt8AN36f5Tk 1GxIltTNtLk4Mivwp9aZ1vf9s5FAhgPDvfGV5yFoKYmA/65ZlrKx0zlFNng= -----END RSA PRIVATE KEY----- httpie-0.9.8/tests/test_binary.py0000644000000000000000000000403413022157774015573 0ustar rootroot"""Tests for dealing with binary request and response data.""" from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG from httpie.compat import urlopen from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from utils import TestEnvironment, http class TestBinaryRequestData: def test_binary_stdin(self, httpbin): with open(BIN_FILE_PATH, 'rb') as stdin: env = TestEnvironment( stdin=stdin, stdin_isatty=False, stdout_isatty=False ) r = http('--print=B', 'POST', httpbin.url + '/post', env=env) assert r == BIN_FILE_CONTENT def test_binary_file_path(self, httpbin): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('--print=B', 'POST', httpbin.url + '/post', '@' + BIN_FILE_PATH_ARG, env=env, ) assert r == BIN_FILE_CONTENT def test_binary_file_form(self, httpbin): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('--print=B', '--form', 'POST', httpbin.url + '/post', 'test@' + BIN_FILE_PATH_ARG, env=env) assert bytes(BIN_FILE_CONTENT) in bytes(r) class TestBinaryResponseData: url = 'http://www.google.com/favicon.ico' @property def bindata(self): if not hasattr(self, '_bindata'): self._bindata = urlopen(self.url).read() return self._bindata def test_binary_suppresses_when_terminal(self): r = http('GET', self.url) assert BINARY_SUPPRESSED_NOTICE.decode() in r def test_binary_suppresses_when_not_terminal_but_pretty(self): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('--pretty=all', 'GET', self.url, env=env) assert BINARY_SUPPRESSED_NOTICE.decode() in r def test_binary_included_and_correct_when_suitable(self): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('GET', self.url, env=env) assert r == self.bindata httpie-0.9.8/tests/test_stream.py0000644000000000000000000000321313022157774015600 0ustar rootrootimport pytest from httpie.compat import is_windows from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from utils import http, TestEnvironment from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH # GET because httpbin 500s with binary POST body. @pytest.mark.skipif(is_windows, reason='Pretty redirect not supported under Windows') def test_pretty_redirected_stream(httpbin): """Test that --stream works with prettified redirected output.""" with open(BIN_FILE_PATH, 'rb') as f: env = TestEnvironment(colors=256, stdin=f, stdin_isatty=False, stdout_isatty=False) r = http('--verbose', '--pretty=all', '--stream', 'GET', httpbin.url + '/get', env=env) assert BINARY_SUPPRESSED_NOTICE.decode() in r def test_encoded_stream(httpbin): """Test that --stream works with non-prettified redirected terminal output.""" with open(BIN_FILE_PATH, 'rb') as f: env = TestEnvironment(stdin=f, stdin_isatty=False) r = http('--pretty=none', '--stream', '--verbose', 'GET', httpbin.url + '/get', env=env) assert BINARY_SUPPRESSED_NOTICE.decode() in r def test_redirected_stream(httpbin): """Test that --stream works with non-prettified redirected terminal output.""" with open(BIN_FILE_PATH, 'rb') as f: env = TestEnvironment(stdout_isatty=False, stdin_isatty=False, stdin=f) r = http('--pretty=none', '--stream', '--verbose', 'GET', httpbin.url + '/get', env=env) assert BIN_FILE_CONTENT in r httpie-0.9.8/tests/test_defaults.py0000644000000000000000000001026713022157774016123 0ustar rootroot""" Tests for the provided defaults regarding HTTP method, and --json vs. --form. """ from httpie.client import JSON_ACCEPT from utils import TestEnvironment, http, HTTP_OK from fixtures import FILE_PATH class TestImplicitHTTPMethod: def test_implicit_GET(self, httpbin): r = http(httpbin.url + '/get') assert HTTP_OK in r def test_implicit_GET_with_headers(self, httpbin): r = http(httpbin.url + '/headers', 'Foo:bar') assert HTTP_OK in r assert r.json['headers']['Foo'] == 'bar' def test_implicit_POST_json(self, httpbin): r = http(httpbin.url + '/post', 'hello=world') assert HTTP_OK in r assert r.json['json'] == {'hello': 'world'} def test_implicit_POST_form(self, httpbin): r = http('--form', httpbin.url + '/post', 'foo=bar') assert HTTP_OK in r assert r.json['form'] == {'foo': 'bar'} def test_implicit_POST_stdin(self, httpbin): with open(FILE_PATH) as f: env = TestEnvironment(stdin_isatty=False, stdin=f) r = http('--form', httpbin.url + '/post', env=env) assert HTTP_OK in r class TestAutoContentTypeAndAcceptHeaders: """ Test that Accept and Content-Type correctly defaults to JSON, but can still be overridden. The same with Content-Type when --form -f is used. """ def test_GET_no_data_no_auto_headers(self, httpbin): # https://github.com/jkbrzt/httpie/issues/62 r = http('GET', httpbin.url + '/headers') assert HTTP_OK in r assert r.json['headers']['Accept'] == '*/*' assert 'Content-Type' not in r.json['headers'] def test_POST_no_data_no_auto_headers(self, httpbin): # JSON headers shouldn't be automatically set for POST with no data. r = http('POST', httpbin.url + '/post') assert HTTP_OK in r assert '"Accept": "*/*"' in r assert '"Content-Type": "application/json' not in r def test_POST_with_data_auto_JSON_headers(self, httpbin): r = http('POST', httpbin.url + '/post', 'a=b') assert HTTP_OK in r assert r.json['headers']['Accept'] == JSON_ACCEPT assert r.json['headers']['Content-Type'] == 'application/json' def test_GET_with_data_auto_JSON_headers(self, httpbin): # JSON headers should automatically be set also for GET with data. r = http('POST', httpbin.url + '/post', 'a=b') assert HTTP_OK in r assert r.json['headers']['Accept'] == JSON_ACCEPT assert r.json['headers']['Content-Type'] == 'application/json' def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin): r = http('--json', 'POST', httpbin.url + '/post') assert HTTP_OK in r assert r.json['headers']['Accept'] == JSON_ACCEPT # Make sure Content-Type gets set even with no data. # https://github.com/jkbrzt/httpie/issues/137 assert 'application/json' in r.json['headers']['Content-Type'] def test_GET_explicit_JSON_explicit_headers(self, httpbin): r = http('--json', 'GET', httpbin.url + '/headers', 'Accept:application/xml', 'Content-Type:application/xml') assert HTTP_OK in r assert '"Accept": "application/xml"' in r assert '"Content-Type": "application/xml"' in r def test_POST_form_auto_Content_Type(self, httpbin): r = http('--form', 'POST', httpbin.url + '/post') assert HTTP_OK in r assert '"Content-Type": "application/x-www-form-urlencoded' in r def test_POST_form_Content_Type_override(self, httpbin): r = http('--form', 'POST', httpbin.url + '/post', 'Content-Type:application/xml') assert HTTP_OK in r assert '"Content-Type": "application/xml"' in r def test_print_only_body_when_stdout_redirected_by_default(self, httpbin): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('GET', httpbin.url + '/get', env=env) assert 'HTTP/' not in r def test_print_overridable_when_stdout_redirected(self, httpbin): env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) r = http('--print=h', 'GET', httpbin.url + '/get', env=env) assert HTTP_OK in r httpie-0.9.8/tests/test_regressions.py0000644000000000000000000000121213022157774016645 0ustar rootroot"""Miscellaneous regression tests""" import pytest from utils import http, HTTP_OK from httpie.compat import is_windows def test_Host_header_overwrite(httpbin): """ https://github.com/jkbrzt/httpie/issues/235 """ host = 'httpbin.org' url = httpbin.url + '/get' r = http('--print=hH', url, 'host:{0}'.format(host)) assert HTTP_OK in r assert r.lower().count('host:') == 1 assert 'host: {0}'.format(host) in r @pytest.mark.skipif(is_windows, reason='Unix-only') def test_output_devnull(httpbin): """ https://github.com/jkbrzt/httpie/issues/252 """ http('--output=/dev/null', httpbin + '/get') httpie-0.9.8/tests/fixtures/0000755000000000000000000000000013022157774014546 5ustar rootroothttpie-0.9.8/tests/fixtures/test.txt0000644000000000000000000000032413022157774016265 0ustar rootroot[one line of UTF8-encoded unicode text] χρυσαφὶ 太陽 เลิศ ♜♞♝♛♚♝♞♜ оживлённым तान्यहानि 有朋 ஸ்றீனிவாஸ ٱلرَّحْمـَبنِ httpie-0.9.8/tests/fixtures/__init__.py0000644000000000000000000000175213022157774016664 0ustar rootroot"""Test data""" from os import path import codecs def patharg(path): """ Back slashes need to be escaped in ITEM args, even in Windows paths. """ return path.replace('\\', '\\\\\\') FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__))) FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt') JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json') BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin') FILE_PATH_ARG = patharg(FILE_PATH) BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH) JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH) with codecs.open(FILE_PATH, encoding='utf8') as f: # Strip because we don't want new lines in the data so that we can # easily count occurrences also when embedded in JSON (where the new # line would be escaped). FILE_CONTENT = f.read().strip() with codecs.open(JSON_FILE_PATH, encoding='utf8') as f: JSON_FILE_CONTENT = f.read() with open(BIN_FILE_PATH, 'rb') as f: BIN_FILE_CONTENT = f.read() UNICODE = FILE_CONTENT httpie-0.9.8/tests/fixtures/test.json0000644000000000000000000000025113022157774016416 0ustar rootroot{ "name": "Jakub Roztočil", "unicode": "χρυσαφὶ 太陽 เลิศ ♜♞♝♛♚♝♞♜ оживлённым तान्यहानि 有朋" } httpie-0.9.8/tests/fixtures/test.bin0000644000000000000000000000217613022157774016225 0ustar rootroot h(  |OEMIx4z?|@|<{Az_ |ʩ1v;?>}>zCy /J9A<@={7 JU%4;=C>}8 *? 9<=>>лmI*8;9<<I"^8~VnkF-<;;===Y'UJHQ&-9;8;==8VOoBڿw577<<8=>^'־޿,;:79:?Bl(T]Ϲ p7=;6 -e:+ҩSdXȐY()& |;]fZΎWPSl$cfYүFFPWcb _ LѬ:?httpie-0.9.8/requirements-dev.txt0000644000000000000000000000010013022157774015562 0ustar rootroottox mock pytest pytest-cov pytest-httpbin>=0.0.6 docutils wheel