clicksearch


Nameclicksearch JSON
Version 0.3.0 PyPI version JSON
download
home_pageNone
SummaryFramework for CLI applications that filter data streams
upload_time2024-06-05 10:48:53
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseCopyright 2023 Petter Nyström 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.
keywords click
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Clicksearch

Clicksearch is a framework for writing CLI programs that filter a stream of data objects. Clicksearch lets you define a model of the objects your program should work with, and based on this model Clicksearch creates a CLI with options for filtering on the defined fields.

Clicksearch is based on the [Click](https://click.palletsprojects.com) framework, which handles all of the heavy lifting CLI work.

## The Basics

Let's start with a basic example on how to write a simple Clicksearch program.

### The Model

At the heart of Clicksearch is the model. Every Clicksearch program needs to define a subclass of the `ModelBase` class, that describes the supported data:

```python
class Person(ModelBase):
    name = Text()
    age = Number()
```

From this simple model you can launch your CLI program by calling the `ModelBase.cli` class method:

```pycon
>>> Person.cli('--help')
Usage: ... [OPTIONS] [FILE]...

Options:
  -v, --verbose  Show more data.
  --brief        Show one line of data, regardless the level of verbose.
  --long         Show multiple lines of data, regardless the level of verbose.
  --show FIELD   Show given field only. Can be repeated to show multiple
                 fields in given order.
  --case         Use case sensitive filtering.
  --exact        Use exact match filtering.
  --regex        Use regular rexpressions when filtering.
  --or FIELD     Treat multiple tests for given field with logical
                 disjunction, i.e. OR-logic instead of AND-logic.
  --inclusive    Treat multiple tests for different fields with logical
                 disjunction, i.e. OR-logic instead of AND-logic.
  --sort FIELD   Sort results by given field.
  --desc         Sort results in descending order.
  --group FIELD  Group results by given field.
  --count FIELD  Print a breakdown of all values for given field.
  --version      Show the version and exit.
  --help         Show this message and exit.

Field filters:
  --name TEXT   Filter on matching name.
  --age NUMBER  Filter on matching age (number comparison).

Where:
  FIELD   One of: age, name.
  NUMBER  A number optionally prefixed by one of the supported comparison
          operators: ==, =, !=, !, <=, <, >=, >. With == being the default if
          only a number is given.
  TEXT    A text partially matching the field value. The --case, --regex and
          --exact options can be applied. If prefixed with ! the match is
          negated.
```

> :exclamation: The first argument to `Person.cli` is the command line arguments as a string. This is optional and generally not required when launching the program from a terminal, but here we need it since we are launching from the Python REPL.

We can see from the `--help` output that we have a bunch of basic options, that will be the same for all Clicksearch programs, and then we have a a few options called *field filters*, that are based on the fields defined on the model.

### The Reader

The next thing Clicksearch needs is a data source, called a _reader_. In Python terms the reader should be a `Callable[[Mapping], Iterable[Mapping]]` object. That is: it should be a callable object that takes a single `dict` argument (the parsed [Click](https://click.palletsprojects.com) parameters) and returns some sort of object that can be iterated over to generate the data objects that Clicksearch should work with.

In its simplest form this can be a function that returns, for instance, a `list`:

```python
def people(options: dict):
    return [
        {'name': 'Alice Anderson', 'age': 42},
        {'name': 'Bob Balderson', 'age': 27},
    ]
```

Or perhaps be a Python generator:

```python
def people(options: dict):
    yield {'name': 'Alice Anderson', 'age': 42}
    yield {'name': 'Bob Balderson', 'age': 27}
```

Provide the reader to `Person.cli` with the `reader` keyword argument. Now you are ready to start using the CLI program! Call the `Person.cli` method with the command line options as the first argument:

```pycon
>>> Person.cli('', reader=people)
Alice Anderson: Age 42.
Bob Balderson: Age 27.

Total count: 2
```

```pycon
>>> Person.cli('--age 27', reader=people)
Bob Balderson
Age: 27

Total count: 1
```

### The Script

Your complete CLI program would then look something like this:

[DOCTEST_BREAK]::

```python
#!/usr/bin/env python3

from clicksearch import ModelBase, Text, Number

class Person(ModelBase):
    name = Text()
    age = Number()

def people(options: dict):
    yield {'name': 'Alice Anderson', 'age': 42}
    yield {'name': 'Bob Balderson', 'age': 27}

if __name__ == '__main__':
    Person.cli(reader=people)
```

[DOCTEST_CONTINUE]::

## Command Line Options

These are the basic command line options available in all Clicksearch programs.

To examplify the different use cases, the following model and reader will be used:

```python
class Employee(ModelBase):
    name = Text()
    title = Text()
    gender = Choice(["Female", "Male", "Other"])
    salary = Number()


def employees(options: dict):
    yield {
        'name': 'Alice Anderson',
        'title': 'Sales Director',
        'salary': 4200,
        'gender': 'Female',
    }
    yield {
        'name': 'Bob Balderson',
        'title': 'Sales Representative',
        'salary': 2700,
        'gender': 'Male',
    }
    yield {
        'name': 'Charlotte Carlson',
        'title': 'Sales Representative',
        'salary': 2200,
        'gender': 'Female',
    }
    yield {
        'name': 'Totoro',
        'title': 'Company Mascot',
    }
```

### `-v`, `--verbose`

The `--verbose` option is used to show more details of the resulting items. By default items are shown using the "brief" format, using a single line per item. Adding a level of verbose will switch to using the "long" format, using a single line per item field.

See also the `verbosity` field parameter for further use cases of the `--verbose` option.

```pycon
>>> Employee.cli('--verbose', reader=employees)
Alice Anderson
Title: Sales Director
Gender: Female
Salary: 4200

Bob Balderson
Title: Sales Representative
Gender: Male
Salary: 2700

Charlotte Carlson
Title: Sales Representative
Gender: Female
Salary: 2200

Totoro
Title: Company Mascot

Total count: 4
```

### `--brief`

The `--brief` option forces the use of the "brief" format, using a single line per item, regardless of the level of verbose. This is mainly useful to ensure that the brief format is used also when a single item is found.

```pycon
>>> Employee.cli('--gender male --brief', reader=employees)
Bob Balderson: Sales Representative. Male. Salary 2700.

Total count: 1
```

### `--long`

The `--long` option forces the use of the "long" format, using a single line per item field, regardless of the level of verbose.

```pycon
>>> Employee.cli('--long', reader=employees)
Alice Anderson
Title: Sales Director
Gender: Female
Salary: 4200

Bob Balderson
Title: Sales Representative
Gender: Male
Salary: 2700

Charlotte Carlson
Title: Sales Representative
Gender: Female
Salary: 2200

Totoro
Title: Company Mascot

Total count: 4
```

### `--show`

The `--show` option can be used to control what fields to display.

```pycon
>>> Employee.cli('--show gender --show salary', reader=employees)
Alice Anderson: Female. Salary 4200.
Bob Balderson: Male. Salary 2700.
Charlotte Carlson: Female. Salary 2200.
Totoro:

Total count: 4
```

```pycon
>>> Employee.cli('--show salary --show title --long', reader=employees)
Alice Anderson
Salary: 4200
Title: Sales Director

Bob Balderson
Salary: 2700
Title: Sales Representative

Charlotte Carlson
Salary: 2200
Title: Sales Representative

Totoro
Title: Company Mascot

Total count: 4
```

### `--case`

The `--case` option makes the `Text` field filter case sensitive.

```pycon
>>> Employee.cli('--name "bob" --case', reader=employees)

Total count: 0
```

```pycon
>>> Employee.cli('--name "Bob" --case', reader=employees)
Bob Balderson
Title: Sales Representative
Gender: Male
Salary: 2700

Total count: 1
```

### `--exact`

The `--exact` option makes the `Text` field filter require a full match.

```pycon
>>> Employee.cli('--name "bob" --exact', reader=employees)

Total count: 0
```

```pycon
>>> Employee.cli('--name "bob balderson" --exact', reader=employees)
Bob Balderson
Title: Sales Representative
Gender: Male
Salary: 2700

Total count: 1
```

### `--regex`

The `--regex` option makes the `Text` field filter operate as a [regular expression](https://docs.python.org/3/library/re.html).

```pycon
>>> Employee.cli('--name "\\b[anderson]+\\b" --regex', reader=employees)
Alice Anderson
Title: Sales Director
Gender: Female
Salary: 4200

Total count: 1
```

```pycon
>>> Employee.cli('--name "\\b[blanderson]+\\b" --regex', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male. Salary 2700.

Total count: 2
```

```pycon
>>> Employee.cli('--name "b]d r[g}x" --regex', reader=employees)
Usage: ...

Error: Invalid value for '--name': Invalid regular expression
```

### `--or`

The `--or` option treats multiple uses of a given field filter as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default unless the field is specifically configured as a inclusive field.

Without `--or`, multiple uses of the same field filter give fewer results.

```pycon
>>> Employee.cli('--name "C" --name "Anderson" --brief', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.

Total count: 1
```

Compared to when `--or` is used:

```pycon
>>> Employee.cli('--name "C" --name "Anderson" --or name --brief', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 2
```

### `--inclusive`

The `--inclusive` option treats multiple uses of different field filters as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default.

Without `--inclusive`, multiple uses of different filters give fewer results:

```pycon
>>> Employee.cli('--gender female --title "sales rep" --brief', reader=employees)
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 1
```

Compared to when `--inclusive` is used:

```pycon
>>> Employee.cli('--gender female --title "sales rep" --inclusive', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male. Salary 2700.
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 3
```

### `--sort`

The `--sort` option controls the order in which resulting items are displayed.

```pycon
>>> Employee.cli('--sort salary', reader=employees)
Totoro: Company Mascot.
Charlotte Carlson: Sales Representative. Female. Salary 2200.
Bob Balderson: Sales Representative. Male. Salary 2700.
Alice Anderson: Sales Director. Female. Salary 4200.

Total count: 4
```

```pycon
>>> Employee.cli('--sort gender', reader=employees)
Totoro: Company Mascot.
Alice Anderson: Sales Director. Female. Salary 4200.
Charlotte Carlson: Sales Representative. Female. Salary 2200.
Bob Balderson: Sales Representative. Male. Salary 2700.

Total count: 4
```

### `--desc`

The `--desc` option switches the `--sort` and `--group` options to use descending order.

```pycon
>>> Employee.cli('--sort salary --desc', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male. Salary 2700.
Charlotte Carlson: Sales Representative. Female. Salary 2200.
Totoro: Company Mascot.

Total count: 4
```

### `--group`

The `--group` option displays the resulting items in groups by the target field values.

```pycon
>>> Employee.cli('--group title', reader=employees)
[ Company Mascot ]

Totoro: Company Mascot.

[ Sales Director ]

Alice Anderson: Sales Director. Female. Salary 4200.

[ Sales Representative ]

Bob Balderson: Sales Representative. Male. Salary 2700.
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 4
```

### `--count`

The `--count` options adds a breakdown of all values for a given field.

```pycon
>>> Employee.cli('--count title', reader=employees)
[ Title counts ]

Sales Representative: 2
Sales Director:       1
Company Mascot:       1

Total count: 4
```

## Fields

Fields are the objects used to compose your model. Clicksearch comes with a number of basic field types built-in, but you can of course also define your own field type by subclassing from the `FieldBase` class (or from any other built-in field type).

### Text

`Text` fields support `str` values and implement a single filter option that matches any part of the field value.

For examples of this field in use see any of the previous sections, and especially those of the `--case`, `--exact` and `--regex` command line options.

### Number

`Number` fields support numeric values and implement a single filter that allows basic comparisons with the field value. In the example below the option will be given the default name `--age`. The supported comparison operators are: `==` (the default), `!=`, `<`, `<=`, `>` and `>=`.

The examples below use the same `Person` model and reader from previous section.

```pycon
>>> Person.cli('--age 42', reader=people)
Alice Anderson
Age: 42

Total count: 1
```

```pycon
>>> Person.cli('--age "<50"', reader=people)
Alice Anderson: Age 42.
Bob Balderson: Age 27.

Total count: 2
```

```pycon
>>> Person.cli('--age ">=42"', reader=people)
Alice Anderson
Age: 42

Total count: 1
```

```pycon
>>> Person.cli('--age "X"', reader=people)
Usage: ...

Error: Invalid value for '--age': X
```

#### `specials`

`Number` fields can also be configured to accept non-numeric values with the `specials` parameter. Such special values only support direct equality comparison.

```python
class Gift(ModelBase):
    name = Text()
    price = Number(specials=['X'])

def gifts(options: dict):
    yield {'name': 'Socks', 'price': 7}
    yield {'name': 'Voucher', 'price': 'X'}
```

```pycon
>>> Gift.cli('', reader=gifts)
Socks: Price 7.
Voucher: Price X.

Total count: 2
```

```pycon
>>> Gift.cli('--price X', reader=gifts)
Voucher
Price: X

Total count: 1
```

```pycon
>>> Gift.cli('--price ">0"', reader=gifts)
Socks
Price: 7

Total count: 1
```

### Count

`Count` behave like `Number` fields but switch the label and value around in the brief format. If the name of the field is one that can have a count before it, then it is probably a `Count` rather than a `Number`.


```python
class Inventory(ModelBase):
    name = Text()
    price = Number()
    in_stock = Count()

def products(options: dict):
    yield {'name': 'Milk', 'price': 7, 'in_stock': 29}
    yield {'name': 'Yoghurt', 'price': 11, 'in_stock': 15}
```

```pycon
>>> Inventory.cli('', reader=products)
Milk: Price 7. 29 In Stock.
Yoghurt: Price 11. 15 In Stock.

Total count: 2
```

### DelimitedText

`DelimitedText` fields behave like a list of `Text` fields, where each part is separated by a given `str` delimiter. Each part is then matched individually.

```python
class Recipe(ModelBase):
    name = Text()
    ingredients = DelimitedText(delimiter=",", optname="ingredient")

def recipes(options: dict):
    yield {"name": "Sandwich", "ingredients": "bread,cheese"}
    yield {"name": "Hamburger", "ingredients": "bread,meat,dressing"}
    yield {"name": "Beef Wellington", "ingredients": "meat,ham,mushrooms,pastry"}
```

```pycon
>>> Recipe.cli('--exact --ingredient bread', reader=recipes)
Sandwich: bread,cheese.
Hamburger: bread,meat,dressing.

Total count: 2
```

```pycon
>>> Recipe.cli('--exact --ingredient mushrooms', reader=recipes)
Beef Wellington
Ingredients: meat,ham,mushrooms,pastry

Total count: 1
```

This also works with negated text matching:

```pycon
>>> Recipe.cli('--exact --ingredient "!cheese" --ingredient "!pastry"', reader=recipes)
Hamburger
Ingredients: bread,meat,dressing

Total count: 1
```

### Choice

`Choice` fields behave like `Text` fields but have a defined set of valid values. Prefix arguments are automatically completed to the valid choice.

```python
class Person(ModelBase):
    name = Text()
    gender = Choice(["Female", "Male", "Other"])

def people(options: dict):
    yield {"name": "Alice Anderson", "gender": "Female"}
    yield {"name": "Bob Balderson", "gender": "Male"}
```

```pycon
>>> Person.cli('', reader=people)
Alice Anderson: Female.
Bob Balderson: Male.

Total count: 2
```

```pycon
>>> Person.cli('--gender male', reader=people)
Bob Balderson
Gender: Male

Total count: 1
```

```pycon
>>> Person.cli('--gender f', reader=people)
Alice Anderson
Gender: Female

Total count: 1
```

```pycon
>>> Person.cli('--gender foo', reader=people)
Usage: ...

Error: Invalid value for '--gender': Valid choices are: female, male, other
```

### Flag

`Flag` fields represent boolean "Yes" or "No" values. A value of `1`, `"1"` or `True` are treated as "Yes", otherwise it's a "No". `Flag` fields implement two filters, one to test for "Yes" values and one for "No" values, the latter prefixed with "non-".

```python
class Person(ModelBase):
    name = Text()
    alive = Flag()

def people(options: dict):
    yield {"name": "Bob Balderson", "alive": 1}
    yield {"name": "Alice Anderson", "alive": 0}
```

```pycon
>>> Person.cli('', reader=people)
Bob Balderson: Alive.
Alice Anderson: Non-Alive.

Total count: 2
```

```pycon
>>> Person.cli('--alive', reader=people)
Bob Balderson
Alive: Yes

Total count: 1
```

```pycon
>>> Person.cli('--non-alive', reader=people)
Alice Anderson
Alive: No

Total count: 1
```

#### `truename` and `falsename`

The `truename` and `falsename` options can be used to configure what is displayed when the `Flag` value is true or false, respectively.

```python
class Person(ModelBase):
    name = Text()
    alive = Flag(truename="Alive and kickin'", falsename="Dead as a dojo")
```

```pycon
>>> Person.cli('', reader=people)
Bob Balderson: Alive and kickin'.
Alice Anderson: Dead as a dojo.

Total count: 2
```

### MarkupText

`MarkupText` fields represent text fields that have HTML-like markup that
should be parsed. HTML-like tags in the values will be replaced with ASCII
styles before displayed.

```python
class WebPage(ModelBase):
    url = Text(realname="URL")
    body = MarkupText()

def pages(options: dict):
    yield {"url": "https://thecompany.com", "body": "<h1>The Company</h1>\nWelcome to our <b>company</b>!"}
```

```pycon
>>> WebPage.cli('', reader=pages)
https://thecompany.com
Body: The Company
Welcome to our company!

Total count: 1
```

```pycon
>>> WebPage.cli('--body "our company"', reader=pages)
https://thecompany.com
Body: The Company
Welcome to our company!

Total count: 1
```

```pycon
>>> WebPage.cli('--body "<b>"', reader=pages)

Total count: 0
```

### FieldBase

`FieldBase` is the base class of all other fields, and not generally intended for direct use in models. The parameters available on `FieldBase` -- and therefore all other fields -- are listed below.

#### `default`

Define a default value used for fields where the value is missing.

```python
class Person(ModelBase):
    name = Text()
    gender = Choice(["Female", "Male", "Other"], default="Other")

def people(options: dict):
    yield {"name": "Totoro"}
```

```pycon
>>> Person.cli('', reader=people)
Totoro
Gender: Other

Total count: 1
```

#### `inclusive`

Treat multiple uses of this field's filters as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default.

Using the example of `Employee` from above with a slightly updated model:

```python
class Employee(ModelBase):
    name = Text()
    title = Text(inclusive=True)
    gender = Choice(["Female", "Male", "Other"], inclusive=True, default="Other")
    salary = Number()
```

Multiple use of `--name` gives fewer results:

```pycon
>>> Employee.cli('--name erson', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male. Salary 2700.

Total count: 2
```

```pycon
>>> Employee.cli('--name erson --name and --brief', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.

Total count: 1
```

But multiple uses of `--gender` gives more results, since it has `inclusive=True`:

```pycon
>>> Employee.cli('--gender other --gender male', reader=employees)
Bob Balderson: Sales Representative. Male. Salary 2700.
Totoro: Company Mascot. Other.

Total count: 2
```

Same with multiple use of `--title`:

```pycon
>>> Employee.cli('--title rep --title dir', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male. Salary 2700.
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 3
```

However, mixed use of `--gender` and `--title` are *not* mutually inclusive.

```pycon
>>> Employee.cli('--title rep --title dir --gender female', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Charlotte Carlson: Sales Representative. Female. Salary 2200.

Total count: 2
```

#### `skip_filters`

Don't add the given filter option for this field.

```python
class Person(ModelBase):
    name = Text()
    age = Number()
    height = Number(skip_filters=[Number.filter_number])
```

```pycon
>>> Person.cli('--help')
Usage: ...

Options: ...

Field filters:
  --name TEXT   Filter on matching name.
  --age NUMBER  Filter on matching age (number comparison).
...
```

#### `keyname`

The item key for getting this field's value. Defaults to the the field property name if not set.

```python
class Event(ModelBase):
    name = Text()
    date = Text(keyname="ISO-8601")

def events(options: dict):
    yield {'name': 'Battle of Hastings', 'ISO-8601': '1066-10-14T13:07:53+0000'}
    yield {'name': '9/11', 'ISO-8601': '2001-09-11T08:46:00-0500'}
```

```pycon
>>> Event.cli('--help', reader=events)
Usage: ...

Options: ...

Field filters:
  --name TEXT  Filter on matching name.
  --date TEXT  Filter on matching date.
...
```

```pycon
>>> Event.cli('-v', reader=events)
Battle of Hastings
Date: 1066-10-14T13:07:53+0000

9/11
Date: 2001-09-11T08:46:00-0500

Total count: 2
```

#### `realname`

The name used to reference the field in command output. Defaults to a title-case version of the field property name with `_` replaced with ` `.

```python
class Event(ModelBase):
    name = Text()
    ISO_8601 = Text(realname="Date")

def events(options: dict):
    yield {'name': 'Battle of Hastings', 'ISO_8601': '1066-10-14T13:07:53+0000'}
    yield {'name': '9/11', 'ISO_8601': '2001-09-11T08:46:00-0500'}
```

```pycon
>>> Event.cli('--help', reader=events)
Usage: ...

Options: ...

Field filters:
  --name TEXT  Filter on matching name.
  --date TEXT  Filter on matching date.
...
```

```pycon
>>> Event.cli('-v', reader=events)
Battle of Hastings
Date: 1066-10-14T13:07:53+0000

9/11
Date: 2001-09-11T08:46:00-0500

Total count: 2
```

#### `helpname`

The name used to substitute the `{helpname}` variable in field filter help texts. Defaults to a lower case version of `realname`.

```python
class Event(ModelBase):
    name = Text()
    ISO_8601 = Text(helpname="date")
```


```pycon
>>> Event.cli('--help', reader=events)
Usage: ...

Options: ...

Field filters:
  --name     TEXT  Filter on matching name.
  --iso-8601 TEXT  Filter on matching date.
...
```

```pycon
>>> Event.cli('-v', reader=events)
Battle of Hastings
Iso 8601: 1066-10-14T13:07:53+0000

9/11
Iso 8601: 2001-09-11T08:46:00-0500

Total count: 2
```

#### `optname`

The name used to substitute the `{optname}` variable in field filter arguments. Defaults to a lower case version of `realname` with ` ` replaced with `-`.

```python
class Event(ModelBase):
    name = Text()
    ISO_8601 = Text(optname="date")
```


```pycon
>>> Event.cli('--help', reader=events)
Usage: ...

Options: ...

Field filters:
  --name TEXT  Filter on matching name.
  --date TEXT  Filter on matching iso 8601.
...
```

```pycon
>>> Event.cli('-v', reader=events)
Battle of Hastings
Iso 8601: 1066-10-14T13:07:53+0000

9/11
Iso 8601: 2001-09-11T08:46:00-0500

Total count: 2
```

#### `optalias`

An alternative option name to use, typically when a short version is required.

```python
class Employee(ModelBase):
    name = Text()
    title = Text(inclusive=True)
    gender = Choice(["Female", "Male", "Other"], inclusive=True, default="Other", optalias="-g")
    salary = Number()
```

```pycon
>>> Employee.cli('--help', reader=employees)
Usage: ...

Options: ...

Field filters:
  --name TEXT           Filter on matching name.
  --title TEXT          Filter on matching title.
  -g, --gender GENDER   Filter on matching gender.
  --gender-isnt GENDER  Filter on non-matching gender.
  --salary NUMBER       Filter on matching salary (number comparison).
...
```

```pycon
>>> Employee.cli('-g Other', reader=employees)
Totoro
Title: Company Mascot
Gender: Other

Total count: 1
```

#### `typename`

The name used in the help text for the argument type of this field. Defaults to the `name` property of the field class.

```python
class Event(ModelBase):
    name = Text()
    ISO_8601 = Text(typename="DATE")
```

```pycon
>>> Event.cli('--help', reader=events)
Usage: ...

Options: ...

Field filters:
  --name     TEXT  Filter on matching name.
  --iso-8601 DATE  Filter on matching iso 8601.
...
```

```pycon
>>> Event.cli('-v', reader=events)
Battle of Hastings
Iso 8601: 1066-10-14T13:07:53+0000

9/11
Iso 8601: 2001-09-11T08:46:00-0500

Total count: 2
```

#### `verbosity`

The level of `verbose` required for this field to be included in the output.

```python
class Book(ModelBase):
    title = Text()
    author = Text()
    author_sorted = Text(verbosity=2)
    pages = Count(verbosity=1)

def books(options: dict):
    yield {'title': 'Moby Dick', 'author': 'Herman Melville', 'author_sorted': 'Melville, Herman', 'pages': 720}
    yield {'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'author_sorted': 'Austen, Jane', 'pages': 416}
```

The fields `pages` and `author_sorted` are not shown with default level of `verbose`:

```pycon
>>> Book.cli('', reader=books)
Moby Dick: Herman Melville.
Pride and Prejudice: Jane Austen.

Total count: 2
```

With 1 level of `verbose` we see that `pages` is shown:

```pycon
>>> Book.cli('-v', reader=books)
Moby Dick
Author: Herman Melville
Pages: 720

Pride and Prejudice
Author: Jane Austen
Pages: 416

Total count: 2
```

With 2 levels of `verbose` we see all the fields:

```pycon
>>> Book.cli('-vv', reader=books)
Moby Dick
Author: Herman Melville
Author Sorted: Melville, Herman
Pages: 720

Pride and Prejudice
Author: Jane Austen
Author Sorted: Austen, Jane
Pages: 416

Total count: 2
```

Note that if a single item is found, the `verbose` level is automatically increased by 1:

```pycon
>>> Book.cli('--author Melville', reader=books)
Moby Dick
Author: Herman Melville
Pages: 720

Total count: 1
```

#### `unlabeled`

Set to `True` to use the values for this field as-is, without its `realname` label.

By default this is set to `True` for the first field defined on a model, otherwise `False`.

```python
class Philosopher(ModelBase):
    name = Text(unlabeled=False)
    quote = Text(unlabeled=True)

def philosophers(options: dict):
    yield {'name': 'Aristotle', 'quote': '"Quality is not an act, it is a habit."'}
    yield {'name': 'Pascal', 'quote': '"You always admire what you don\'t understand."'}
```

```pycon
>>> Philosopher.cli('-v', reader=philosophers)
Name: Aristotle
"Quality is not an act, it is a habit."

Name: Pascal
"You always admire what you don't understand."

Total count: 2
```

#### `implied`

A string specifying a set of default filters to apply when this field is used. The syntax for this string is the same as if the options were given on the command line. The implied filters are only applied if the targeted field does not have any filters set.

```python
class Species(ModelBase):
    name = Text()
    animal_type = Choice(
        ['Mammal', 'Fish', 'Bird', 'Reptile', 'Amphibian'],
        keyname="type",
        optname="type",
        realname="Type",
        inclusive=True,
    )
    gestation_period = Number(
        implied="--type Mammal",
        optname="gp",
    )

def species(options: dict):
    yield {'name': 'Human', 'type': 'Mammal', 'gestation_period': 280}
    yield {'name': 'Cat', 'type': 'Mammal', 'gestation_period': 65}
    yield {'name': 'Eagle', 'type': 'Bird', 'gestation_period': 0}
    yield {'name': 'Toad', 'type': 'Amphibian'}
```

The "Eagle" and the "Toad" are excluded from the output because the `--gp` option implies `--type Mammal`:


```pycon
>>> Species.cli('--gp "<100"', reader=species)
Cat
Type: Mammal
Gestation Period: 65

Total count: 1
```

```pycon
>>> Species.cli('--sort "gestation period"', reader=species)
Cat: Mammal. Gestation Period 65.
Human: Mammal. Gestation Period 280.

Total count: 2
```

```pycon
>>> Species.cli('--group "gestation period"', reader=species)
[ Gestation Period 65 ]
Cat: Mammal. Gestation Period 65.

[ Gestation Period 280 ]
Human: Mammal. Gestation Period 280.

Total count: 2
```

```pycon
>>> Species.cli('--show "gestation period"', reader=species)
Human: Gestation Period 280.
Cat: Gestation Period 65.

Total count: 2
```

```pycon
>>> Species.cli('--count "gestation period"', reader=species)

[ Gestation Period counts ]

Gestation Period 280: 1
Gestation Period 65:  1

Total count: 2
```

If `animal_type` is explicitly filtered then the implied `--type` is ignored:

```pycon
>>> Species.cli('--sort "gestation period" --type-isnt Mammal', reader=species)
Toad: Amphibian.
Eagle: Bird. Gestation Period 0.

Total count: 2
```

#### `redirect_args`

Set to `True` to redirect all positional arguments to the **first** filter option for this field.

```python
class Employee(ModelBase):
    name = Text(redirect_args=True)
    title = Text(inclusive=True)
    gender = Choice(["Female", "Male", "Other"], inclusive=True, default="Other")
    salary = Number()
```

```pycon
>>> Employee.cli('Bob', reader=employees)
Bob Balderson
Title: Sales Representative
Gender: Male
Salary: 2700

Total count: 1
```

#### `prefetch`

Optional `Callable` that can potentially alter an `item` before `fetch` gets the field's value from it. Raise `MissingField` exception to skip the item.

```python
def can_have_salary(item):
    if item["gender"] == "Male":
        raise MissingField("Sorry boys!")
    return item

class Employee(ModelBase):
    name = Text(redirect_args=True)
    title = Text(inclusive=True)
    gender = Choice(["Female", "Male", "Other"], inclusive=True, default="Other")
    salary = Number(prefetch=can_have_salary)
```

```pycon
>>> Employee.cli('', reader=employees)
Alice Anderson: Sales Director. Female. Salary 4200.
Bob Balderson: Sales Representative. Male.
Charlotte Carlson: Sales Representative. Female. Salary 2200.
Totoro: Company Mascot. Other.

Total count: 4
```

#### `styles`

Set the styles with which to display values of this field, as passed on to [click.style](https://click.palletsprojects.com/en/latest/api/#click.style).

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "clicksearch",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": "Petter Nystr\u00f6m <jimorie@gmail.com>",
    "keywords": "click",
    "author": null,
    "author_email": "Petter Nystr\u00f6m <jimorie@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/c2/b2/d19c66161e9bed7143d2e533afb39fa39174b396fe92bb4bf11dce63f306/clicksearch-0.3.0.tar.gz",
    "platform": null,
    "description": "# Clicksearch\n\nClicksearch is a framework for writing CLI programs that filter a stream of data objects. Clicksearch lets you define a model of the objects your program should work with, and based on this model Clicksearch creates a CLI with options for filtering on the defined fields.\n\nClicksearch is based on the [Click](https://click.palletsprojects.com) framework, which handles all of the heavy lifting CLI work.\n\n## The Basics\n\nLet's start with a basic example on how to write a simple Clicksearch program.\n\n### The Model\n\nAt the heart of Clicksearch is the model. Every Clicksearch program needs to define a subclass of the `ModelBase` class, that describes the supported data:\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    age = Number()\n```\n\nFrom this simple model you can launch your CLI program by calling the `ModelBase.cli` class method:\n\n```pycon\n>>> Person.cli('--help')\nUsage: ... [OPTIONS] [FILE]...\n\nOptions:\n  -v, --verbose  Show more data.\n  --brief        Show one line of data, regardless the level of verbose.\n  --long         Show multiple lines of data, regardless the level of verbose.\n  --show FIELD   Show given field only. Can be repeated to show multiple\n                 fields in given order.\n  --case         Use case sensitive filtering.\n  --exact        Use exact match filtering.\n  --regex        Use regular rexpressions when filtering.\n  --or FIELD     Treat multiple tests for given field with logical\n                 disjunction, i.e. OR-logic instead of AND-logic.\n  --inclusive    Treat multiple tests for different fields with logical\n                 disjunction, i.e. OR-logic instead of AND-logic.\n  --sort FIELD   Sort results by given field.\n  --desc         Sort results in descending order.\n  --group FIELD  Group results by given field.\n  --count FIELD  Print a breakdown of all values for given field.\n  --version      Show the version and exit.\n  --help         Show this message and exit.\n\nField filters:\n  --name TEXT   Filter on matching name.\n  --age NUMBER  Filter on matching age (number comparison).\n\nWhere:\n  FIELD   One of: age, name.\n  NUMBER  A number optionally prefixed by one of the supported comparison\n          operators: ==, =, !=, !, <=, <, >=, >. With == being the default if\n          only a number is given.\n  TEXT    A text partially matching the field value. The --case, --regex and\n          --exact options can be applied. If prefixed with ! the match is\n          negated.\n```\n\n> :exclamation: The first argument to `Person.cli` is the command line arguments as a string. This is optional and generally not required when launching the program from a terminal, but here we need it since we are launching from the Python REPL.\n\nWe can see from the `--help` output that we have a bunch of basic options, that will be the same for all Clicksearch programs, and then we have a a few options called *field filters*, that are based on the fields defined on the model.\n\n### The Reader\n\nThe next thing Clicksearch needs is a data source, called a _reader_. In Python terms the reader should be a `Callable[[Mapping], Iterable[Mapping]]` object. That is: it should be a callable object that takes a single `dict` argument (the parsed [Click](https://click.palletsprojects.com) parameters) and returns some sort of object that can be iterated over to generate the data objects that Clicksearch should work with.\n\nIn its simplest form this can be a function that returns, for instance, a `list`:\n\n```python\ndef people(options: dict):\n    return [\n        {'name': 'Alice Anderson', 'age': 42},\n        {'name': 'Bob Balderson', 'age': 27},\n    ]\n```\n\nOr perhaps be a Python generator:\n\n```python\ndef people(options: dict):\n    yield {'name': 'Alice Anderson', 'age': 42}\n    yield {'name': 'Bob Balderson', 'age': 27}\n```\n\nProvide the reader to `Person.cli` with the `reader` keyword argument. Now you are ready to start using the CLI program! Call the `Person.cli` method with the command line options as the first argument:\n\n```pycon\n>>> Person.cli('', reader=people)\nAlice Anderson: Age 42.\nBob Balderson: Age 27.\n\nTotal count: 2\n```\n\n```pycon\n>>> Person.cli('--age 27', reader=people)\nBob Balderson\nAge: 27\n\nTotal count: 1\n```\n\n### The Script\n\nYour complete CLI program would then look something like this:\n\n[DOCTEST_BREAK]::\n\n```python\n#!/usr/bin/env python3\n\nfrom clicksearch import ModelBase, Text, Number\n\nclass Person(ModelBase):\n    name = Text()\n    age = Number()\n\ndef people(options: dict):\n    yield {'name': 'Alice Anderson', 'age': 42}\n    yield {'name': 'Bob Balderson', 'age': 27}\n\nif __name__ == '__main__':\n    Person.cli(reader=people)\n```\n\n[DOCTEST_CONTINUE]::\n\n## Command Line Options\n\nThese are the basic command line options available in all Clicksearch programs.\n\nTo examplify the different use cases, the following model and reader will be used:\n\n```python\nclass Employee(ModelBase):\n    name = Text()\n    title = Text()\n    gender = Choice([\"Female\", \"Male\", \"Other\"])\n    salary = Number()\n\n\ndef employees(options: dict):\n    yield {\n        'name': 'Alice Anderson',\n        'title': 'Sales Director',\n        'salary': 4200,\n        'gender': 'Female',\n    }\n    yield {\n        'name': 'Bob Balderson',\n        'title': 'Sales Representative',\n        'salary': 2700,\n        'gender': 'Male',\n    }\n    yield {\n        'name': 'Charlotte Carlson',\n        'title': 'Sales Representative',\n        'salary': 2200,\n        'gender': 'Female',\n    }\n    yield {\n        'name': 'Totoro',\n        'title': 'Company Mascot',\n    }\n```\n\n### `-v`, `--verbose`\n\nThe `--verbose` option is used to show more details of the resulting items. By default items are shown using the \"brief\" format, using a single line per item. Adding a level of verbose will switch to using the \"long\" format, using a single line per item field.\n\nSee also the `verbosity` field parameter for further use cases of the `--verbose` option.\n\n```pycon\n>>> Employee.cli('--verbose', reader=employees)\nAlice Anderson\nTitle: Sales Director\nGender: Female\nSalary: 4200\n\nBob Balderson\nTitle: Sales Representative\nGender: Male\nSalary: 2700\n\nCharlotte Carlson\nTitle: Sales Representative\nGender: Female\nSalary: 2200\n\nTotoro\nTitle: Company Mascot\n\nTotal count: 4\n```\n\n### `--brief`\n\nThe `--brief` option forces the use of the \"brief\" format, using a single line per item, regardless of the level of verbose. This is mainly useful to ensure that the brief format is used also when a single item is found.\n\n```pycon\n>>> Employee.cli('--gender male --brief', reader=employees)\nBob Balderson: Sales Representative. Male. Salary 2700.\n\nTotal count: 1\n```\n\n### `--long`\n\nThe `--long` option forces the use of the \"long\" format, using a single line per item field, regardless of the level of verbose.\n\n```pycon\n>>> Employee.cli('--long', reader=employees)\nAlice Anderson\nTitle: Sales Director\nGender: Female\nSalary: 4200\n\nBob Balderson\nTitle: Sales Representative\nGender: Male\nSalary: 2700\n\nCharlotte Carlson\nTitle: Sales Representative\nGender: Female\nSalary: 2200\n\nTotoro\nTitle: Company Mascot\n\nTotal count: 4\n```\n\n### `--show`\n\nThe `--show` option can be used to control what fields to display.\n\n```pycon\n>>> Employee.cli('--show gender --show salary', reader=employees)\nAlice Anderson: Female. Salary 4200.\nBob Balderson: Male. Salary 2700.\nCharlotte Carlson: Female. Salary 2200.\nTotoro:\n\nTotal count: 4\n```\n\n```pycon\n>>> Employee.cli('--show salary --show title --long', reader=employees)\nAlice Anderson\nSalary: 4200\nTitle: Sales Director\n\nBob Balderson\nSalary: 2700\nTitle: Sales Representative\n\nCharlotte Carlson\nSalary: 2200\nTitle: Sales Representative\n\nTotoro\nTitle: Company Mascot\n\nTotal count: 4\n```\n\n### `--case`\n\nThe `--case` option makes the `Text` field filter case sensitive.\n\n```pycon\n>>> Employee.cli('--name \"bob\" --case', reader=employees)\n\nTotal count: 0\n```\n\n```pycon\n>>> Employee.cli('--name \"Bob\" --case', reader=employees)\nBob Balderson\nTitle: Sales Representative\nGender: Male\nSalary: 2700\n\nTotal count: 1\n```\n\n### `--exact`\n\nThe `--exact` option makes the `Text` field filter require a full match.\n\n```pycon\n>>> Employee.cli('--name \"bob\" --exact', reader=employees)\n\nTotal count: 0\n```\n\n```pycon\n>>> Employee.cli('--name \"bob balderson\" --exact', reader=employees)\nBob Balderson\nTitle: Sales Representative\nGender: Male\nSalary: 2700\n\nTotal count: 1\n```\n\n### `--regex`\n\nThe `--regex` option makes the `Text` field filter operate as a [regular expression](https://docs.python.org/3/library/re.html).\n\n```pycon\n>>> Employee.cli('--name \"\\\\b[anderson]+\\\\b\" --regex', reader=employees)\nAlice Anderson\nTitle: Sales Director\nGender: Female\nSalary: 4200\n\nTotal count: 1\n```\n\n```pycon\n>>> Employee.cli('--name \"\\\\b[blanderson]+\\\\b\" --regex', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male. Salary 2700.\n\nTotal count: 2\n```\n\n```pycon\n>>> Employee.cli('--name \"b]d r[g}x\" --regex', reader=employees)\nUsage: ...\n\nError: Invalid value for '--name': Invalid regular expression\n```\n\n### `--or`\n\nThe `--or` option treats multiple uses of a given field filter as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default unless the field is specifically configured as a inclusive field.\n\nWithout `--or`, multiple uses of the same field filter give fewer results.\n\n```pycon\n>>> Employee.cli('--name \"C\" --name \"Anderson\" --brief', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\n\nTotal count: 1\n```\n\nCompared to when `--or` is used:\n\n```pycon\n>>> Employee.cli('--name \"C\" --name \"Anderson\" --or name --brief', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 2\n```\n\n### `--inclusive`\n\nThe `--inclusive` option treats multiple uses of different field filters as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default.\n\nWithout `--inclusive`, multiple uses of different filters give fewer results:\n\n```pycon\n>>> Employee.cli('--gender female --title \"sales rep\" --brief', reader=employees)\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 1\n```\n\nCompared to when `--inclusive` is used:\n\n```pycon\n>>> Employee.cli('--gender female --title \"sales rep\" --inclusive', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male. Salary 2700.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 3\n```\n\n### `--sort`\n\nThe `--sort` option controls the order in which resulting items are displayed.\n\n```pycon\n>>> Employee.cli('--sort salary', reader=employees)\nTotoro: Company Mascot.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\nBob Balderson: Sales Representative. Male. Salary 2700.\nAlice Anderson: Sales Director. Female. Salary 4200.\n\nTotal count: 4\n```\n\n```pycon\n>>> Employee.cli('--sort gender', reader=employees)\nTotoro: Company Mascot.\nAlice Anderson: Sales Director. Female. Salary 4200.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\nBob Balderson: Sales Representative. Male. Salary 2700.\n\nTotal count: 4\n```\n\n### `--desc`\n\nThe `--desc` option switches the `--sort` and `--group` options to use descending order.\n\n```pycon\n>>> Employee.cli('--sort salary --desc', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male. Salary 2700.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\nTotoro: Company Mascot.\n\nTotal count: 4\n```\n\n### `--group`\n\nThe `--group` option displays the resulting items in groups by the target field values.\n\n```pycon\n>>> Employee.cli('--group title', reader=employees)\n[ Company Mascot ]\n\nTotoro: Company Mascot.\n\n[ Sales Director ]\n\nAlice Anderson: Sales Director. Female. Salary 4200.\n\n[ Sales Representative ]\n\nBob Balderson: Sales Representative. Male. Salary 2700.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 4\n```\n\n### `--count`\n\nThe `--count` options adds a breakdown of all values for a given field.\n\n```pycon\n>>> Employee.cli('--count title', reader=employees)\n[ Title counts ]\n\nSales Representative: 2\nSales Director:       1\nCompany Mascot:       1\n\nTotal count: 4\n```\n\n## Fields\n\nFields are the objects used to compose your model. Clicksearch comes with a number of basic field types built-in, but you can of course also define your own field type by subclassing from the `FieldBase` class (or from any other built-in field type).\n\n### Text\n\n`Text` fields support `str` values and implement a single filter option that matches any part of the field value.\n\nFor examples of this field in use see any of the previous sections, and especially those of the `--case`, `--exact` and `--regex` command line options.\n\n### Number\n\n`Number` fields support numeric values and implement a single filter that allows basic comparisons with the field value. In the example below the option will be given the default name `--age`. The supported comparison operators are: `==` (the default), `!=`, `<`, `<=`, `>` and `>=`.\n\nThe examples below use the same `Person` model and reader from previous section.\n\n```pycon\n>>> Person.cli('--age 42', reader=people)\nAlice Anderson\nAge: 42\n\nTotal count: 1\n```\n\n```pycon\n>>> Person.cli('--age \"<50\"', reader=people)\nAlice Anderson: Age 42.\nBob Balderson: Age 27.\n\nTotal count: 2\n```\n\n```pycon\n>>> Person.cli('--age \">=42\"', reader=people)\nAlice Anderson\nAge: 42\n\nTotal count: 1\n```\n\n```pycon\n>>> Person.cli('--age \"X\"', reader=people)\nUsage: ...\n\nError: Invalid value for '--age': X\n```\n\n#### `specials`\n\n`Number` fields can also be configured to accept non-numeric values with the `specials` parameter. Such special values only support direct equality comparison.\n\n```python\nclass Gift(ModelBase):\n    name = Text()\n    price = Number(specials=['X'])\n\ndef gifts(options: dict):\n    yield {'name': 'Socks', 'price': 7}\n    yield {'name': 'Voucher', 'price': 'X'}\n```\n\n```pycon\n>>> Gift.cli('', reader=gifts)\nSocks: Price 7.\nVoucher: Price X.\n\nTotal count: 2\n```\n\n```pycon\n>>> Gift.cli('--price X', reader=gifts)\nVoucher\nPrice: X\n\nTotal count: 1\n```\n\n```pycon\n>>> Gift.cli('--price \">0\"', reader=gifts)\nSocks\nPrice: 7\n\nTotal count: 1\n```\n\n### Count\n\n`Count` behave like `Number` fields but switch the label and value around in the brief format. If the name of the field is one that can have a count before it, then it is probably a `Count` rather than a `Number`.\n\n\n```python\nclass Inventory(ModelBase):\n    name = Text()\n    price = Number()\n    in_stock = Count()\n\ndef products(options: dict):\n    yield {'name': 'Milk', 'price': 7, 'in_stock': 29}\n    yield {'name': 'Yoghurt', 'price': 11, 'in_stock': 15}\n```\n\n```pycon\n>>> Inventory.cli('', reader=products)\nMilk: Price 7. 29 In Stock.\nYoghurt: Price 11. 15 In Stock.\n\nTotal count: 2\n```\n\n### DelimitedText\n\n`DelimitedText` fields behave like a list of `Text` fields, where each part is separated by a given `str` delimiter. Each part is then matched individually.\n\n```python\nclass Recipe(ModelBase):\n    name = Text()\n    ingredients = DelimitedText(delimiter=\",\", optname=\"ingredient\")\n\ndef recipes(options: dict):\n    yield {\"name\": \"Sandwich\", \"ingredients\": \"bread,cheese\"}\n    yield {\"name\": \"Hamburger\", \"ingredients\": \"bread,meat,dressing\"}\n    yield {\"name\": \"Beef Wellington\", \"ingredients\": \"meat,ham,mushrooms,pastry\"}\n```\n\n```pycon\n>>> Recipe.cli('--exact --ingredient bread', reader=recipes)\nSandwich: bread,cheese.\nHamburger: bread,meat,dressing.\n\nTotal count: 2\n```\n\n```pycon\n>>> Recipe.cli('--exact --ingredient mushrooms', reader=recipes)\nBeef Wellington\nIngredients: meat,ham,mushrooms,pastry\n\nTotal count: 1\n```\n\nThis also works with negated text matching:\n\n```pycon\n>>> Recipe.cli('--exact --ingredient \"!cheese\" --ingredient \"!pastry\"', reader=recipes)\nHamburger\nIngredients: bread,meat,dressing\n\nTotal count: 1\n```\n\n### Choice\n\n`Choice` fields behave like `Text` fields but have a defined set of valid values. Prefix arguments are automatically completed to the valid choice.\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    gender = Choice([\"Female\", \"Male\", \"Other\"])\n\ndef people(options: dict):\n    yield {\"name\": \"Alice Anderson\", \"gender\": \"Female\"}\n    yield {\"name\": \"Bob Balderson\", \"gender\": \"Male\"}\n```\n\n```pycon\n>>> Person.cli('', reader=people)\nAlice Anderson: Female.\nBob Balderson: Male.\n\nTotal count: 2\n```\n\n```pycon\n>>> Person.cli('--gender male', reader=people)\nBob Balderson\nGender: Male\n\nTotal count: 1\n```\n\n```pycon\n>>> Person.cli('--gender f', reader=people)\nAlice Anderson\nGender: Female\n\nTotal count: 1\n```\n\n```pycon\n>>> Person.cli('--gender foo', reader=people)\nUsage: ...\n\nError: Invalid value for '--gender': Valid choices are: female, male, other\n```\n\n### Flag\n\n`Flag` fields represent boolean \"Yes\" or \"No\" values. A value of `1`, `\"1\"` or `True` are treated as \"Yes\", otherwise it's a \"No\". `Flag` fields implement two filters, one to test for \"Yes\" values and one for \"No\" values, the latter prefixed with \"non-\".\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    alive = Flag()\n\ndef people(options: dict):\n    yield {\"name\": \"Bob Balderson\", \"alive\": 1}\n    yield {\"name\": \"Alice Anderson\", \"alive\": 0}\n```\n\n```pycon\n>>> Person.cli('', reader=people)\nBob Balderson: Alive.\nAlice Anderson: Non-Alive.\n\nTotal count: 2\n```\n\n```pycon\n>>> Person.cli('--alive', reader=people)\nBob Balderson\nAlive: Yes\n\nTotal count: 1\n```\n\n```pycon\n>>> Person.cli('--non-alive', reader=people)\nAlice Anderson\nAlive: No\n\nTotal count: 1\n```\n\n#### `truename` and `falsename`\n\nThe `truename` and `falsename` options can be used to configure what is displayed when the `Flag` value is true or false, respectively.\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    alive = Flag(truename=\"Alive and kickin'\", falsename=\"Dead as a dojo\")\n```\n\n```pycon\n>>> Person.cli('', reader=people)\nBob Balderson: Alive and kickin'.\nAlice Anderson: Dead as a dojo.\n\nTotal count: 2\n```\n\n### MarkupText\n\n`MarkupText` fields represent text fields that have HTML-like markup that\nshould be parsed. HTML-like tags in the values will be replaced with ASCII\nstyles before displayed.\n\n```python\nclass WebPage(ModelBase):\n    url = Text(realname=\"URL\")\n    body = MarkupText()\n\ndef pages(options: dict):\n    yield {\"url\": \"https://thecompany.com\", \"body\": \"<h1>The Company</h1>\\nWelcome to our <b>company</b>!\"}\n```\n\n```pycon\n>>> WebPage.cli('', reader=pages)\nhttps://thecompany.com\nBody: The Company\nWelcome to our company!\n\nTotal count: 1\n```\n\n```pycon\n>>> WebPage.cli('--body \"our company\"', reader=pages)\nhttps://thecompany.com\nBody: The Company\nWelcome to our company!\n\nTotal count: 1\n```\n\n```pycon\n>>> WebPage.cli('--body \"<b>\"', reader=pages)\n\nTotal count: 0\n```\n\n### FieldBase\n\n`FieldBase` is the base class of all other fields, and not generally intended for direct use in models. The parameters available on `FieldBase` -- and therefore all other fields -- are listed below.\n\n#### `default`\n\nDefine a default value used for fields where the value is missing.\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    gender = Choice([\"Female\", \"Male\", \"Other\"], default=\"Other\")\n\ndef people(options: dict):\n    yield {\"name\": \"Totoro\"}\n```\n\n```pycon\n>>> Person.cli('', reader=people)\nTotoro\nGender: Other\n\nTotal count: 1\n```\n\n#### `inclusive`\n\nTreat multiple uses of this field's filters as a [logical disjunction](https://en.wikipedia.org/wiki/Classical_logic) (OR logic), rather than a [logical conjunction](https://en.wikipedia.org/wiki/Logical_conjunction) (AND logic), which is the default.\n\nUsing the example of `Employee` from above with a slightly updated model:\n\n```python\nclass Employee(ModelBase):\n    name = Text()\n    title = Text(inclusive=True)\n    gender = Choice([\"Female\", \"Male\", \"Other\"], inclusive=True, default=\"Other\")\n    salary = Number()\n```\n\nMultiple use of `--name` gives fewer results:\n\n```pycon\n>>> Employee.cli('--name erson', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male. Salary 2700.\n\nTotal count: 2\n```\n\n```pycon\n>>> Employee.cli('--name erson --name and --brief', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\n\nTotal count: 1\n```\n\nBut multiple uses of `--gender` gives more results, since it has `inclusive=True`:\n\n```pycon\n>>> Employee.cli('--gender other --gender male', reader=employees)\nBob Balderson: Sales Representative. Male. Salary 2700.\nTotoro: Company Mascot. Other.\n\nTotal count: 2\n```\n\nSame with multiple use of `--title`:\n\n```pycon\n>>> Employee.cli('--title rep --title dir', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male. Salary 2700.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 3\n```\n\nHowever, mixed use of `--gender` and `--title` are *not* mutually inclusive.\n\n```pycon\n>>> Employee.cli('--title rep --title dir --gender female', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\n\nTotal count: 2\n```\n\n#### `skip_filters`\n\nDon't add the given filter option for this field.\n\n```python\nclass Person(ModelBase):\n    name = Text()\n    age = Number()\n    height = Number(skip_filters=[Number.filter_number])\n```\n\n```pycon\n>>> Person.cli('--help')\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name TEXT   Filter on matching name.\n  --age NUMBER  Filter on matching age (number comparison).\n...\n```\n\n#### `keyname`\n\nThe item key for getting this field's value. Defaults to the the field property name if not set.\n\n```python\nclass Event(ModelBase):\n    name = Text()\n    date = Text(keyname=\"ISO-8601\")\n\ndef events(options: dict):\n    yield {'name': 'Battle of Hastings', 'ISO-8601': '1066-10-14T13:07:53+0000'}\n    yield {'name': '9/11', 'ISO-8601': '2001-09-11T08:46:00-0500'}\n```\n\n```pycon\n>>> Event.cli('--help', reader=events)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name TEXT  Filter on matching name.\n  --date TEXT  Filter on matching date.\n...\n```\n\n```pycon\n>>> Event.cli('-v', reader=events)\nBattle of Hastings\nDate: 1066-10-14T13:07:53+0000\n\n9/11\nDate: 2001-09-11T08:46:00-0500\n\nTotal count: 2\n```\n\n#### `realname`\n\nThe name used to reference the field in command output. Defaults to a title-case version of the field property name with `_` replaced with ` `.\n\n```python\nclass Event(ModelBase):\n    name = Text()\n    ISO_8601 = Text(realname=\"Date\")\n\ndef events(options: dict):\n    yield {'name': 'Battle of Hastings', 'ISO_8601': '1066-10-14T13:07:53+0000'}\n    yield {'name': '9/11', 'ISO_8601': '2001-09-11T08:46:00-0500'}\n```\n\n```pycon\n>>> Event.cli('--help', reader=events)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name TEXT  Filter on matching name.\n  --date TEXT  Filter on matching date.\n...\n```\n\n```pycon\n>>> Event.cli('-v', reader=events)\nBattle of Hastings\nDate: 1066-10-14T13:07:53+0000\n\n9/11\nDate: 2001-09-11T08:46:00-0500\n\nTotal count: 2\n```\n\n#### `helpname`\n\nThe name used to substitute the `{helpname}` variable in field filter help texts. Defaults to a lower case version of `realname`.\n\n```python\nclass Event(ModelBase):\n    name = Text()\n    ISO_8601 = Text(helpname=\"date\")\n```\n\n\n```pycon\n>>> Event.cli('--help', reader=events)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name     TEXT  Filter on matching name.\n  --iso-8601 TEXT  Filter on matching date.\n...\n```\n\n```pycon\n>>> Event.cli('-v', reader=events)\nBattle of Hastings\nIso 8601: 1066-10-14T13:07:53+0000\n\n9/11\nIso 8601: 2001-09-11T08:46:00-0500\n\nTotal count: 2\n```\n\n#### `optname`\n\nThe name used to substitute the `{optname}` variable in field filter arguments. Defaults to a lower case version of `realname` with ` ` replaced with `-`.\n\n```python\nclass Event(ModelBase):\n    name = Text()\n    ISO_8601 = Text(optname=\"date\")\n```\n\n\n```pycon\n>>> Event.cli('--help', reader=events)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name TEXT  Filter on matching name.\n  --date TEXT  Filter on matching iso 8601.\n...\n```\n\n```pycon\n>>> Event.cli('-v', reader=events)\nBattle of Hastings\nIso 8601: 1066-10-14T13:07:53+0000\n\n9/11\nIso 8601: 2001-09-11T08:46:00-0500\n\nTotal count: 2\n```\n\n#### `optalias`\n\nAn alternative option name to use, typically when a short version is required.\n\n```python\nclass Employee(ModelBase):\n    name = Text()\n    title = Text(inclusive=True)\n    gender = Choice([\"Female\", \"Male\", \"Other\"], inclusive=True, default=\"Other\", optalias=\"-g\")\n    salary = Number()\n```\n\n```pycon\n>>> Employee.cli('--help', reader=employees)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name TEXT           Filter on matching name.\n  --title TEXT          Filter on matching title.\n  -g, --gender GENDER   Filter on matching gender.\n  --gender-isnt GENDER  Filter on non-matching gender.\n  --salary NUMBER       Filter on matching salary (number comparison).\n...\n```\n\n```pycon\n>>> Employee.cli('-g Other', reader=employees)\nTotoro\nTitle: Company Mascot\nGender: Other\n\nTotal count: 1\n```\n\n#### `typename`\n\nThe name used in the help text for the argument type of this field. Defaults to the `name` property of the field class.\n\n```python\nclass Event(ModelBase):\n    name = Text()\n    ISO_8601 = Text(typename=\"DATE\")\n```\n\n```pycon\n>>> Event.cli('--help', reader=events)\nUsage: ...\n\nOptions: ...\n\nField filters:\n  --name     TEXT  Filter on matching name.\n  --iso-8601 DATE  Filter on matching iso 8601.\n...\n```\n\n```pycon\n>>> Event.cli('-v', reader=events)\nBattle of Hastings\nIso 8601: 1066-10-14T13:07:53+0000\n\n9/11\nIso 8601: 2001-09-11T08:46:00-0500\n\nTotal count: 2\n```\n\n#### `verbosity`\n\nThe level of `verbose` required for this field to be included in the output.\n\n```python\nclass Book(ModelBase):\n    title = Text()\n    author = Text()\n    author_sorted = Text(verbosity=2)\n    pages = Count(verbosity=1)\n\ndef books(options: dict):\n    yield {'title': 'Moby Dick', 'author': 'Herman Melville', 'author_sorted': 'Melville, Herman', 'pages': 720}\n    yield {'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'author_sorted': 'Austen, Jane', 'pages': 416}\n```\n\nThe fields `pages` and `author_sorted` are not shown with default level of `verbose`:\n\n```pycon\n>>> Book.cli('', reader=books)\nMoby Dick: Herman Melville.\nPride and Prejudice: Jane Austen.\n\nTotal count: 2\n```\n\nWith 1 level of `verbose` we see that `pages` is shown:\n\n```pycon\n>>> Book.cli('-v', reader=books)\nMoby Dick\nAuthor: Herman Melville\nPages: 720\n\nPride and Prejudice\nAuthor: Jane Austen\nPages: 416\n\nTotal count: 2\n```\n\nWith 2 levels of `verbose` we see all the fields:\n\n```pycon\n>>> Book.cli('-vv', reader=books)\nMoby Dick\nAuthor: Herman Melville\nAuthor Sorted: Melville, Herman\nPages: 720\n\nPride and Prejudice\nAuthor: Jane Austen\nAuthor Sorted: Austen, Jane\nPages: 416\n\nTotal count: 2\n```\n\nNote that if a single item is found, the `verbose` level is automatically increased by 1:\n\n```pycon\n>>> Book.cli('--author Melville', reader=books)\nMoby Dick\nAuthor: Herman Melville\nPages: 720\n\nTotal count: 1\n```\n\n#### `unlabeled`\n\nSet to `True` to use the values for this field as-is, without its `realname` label.\n\nBy default this is set to `True` for the first field defined on a model, otherwise `False`.\n\n```python\nclass Philosopher(ModelBase):\n    name = Text(unlabeled=False)\n    quote = Text(unlabeled=True)\n\ndef philosophers(options: dict):\n    yield {'name': 'Aristotle', 'quote': '\"Quality is not an act, it is a habit.\"'}\n    yield {'name': 'Pascal', 'quote': '\"You always admire what you don\\'t understand.\"'}\n```\n\n```pycon\n>>> Philosopher.cli('-v', reader=philosophers)\nName: Aristotle\n\"Quality is not an act, it is a habit.\"\n\nName: Pascal\n\"You always admire what you don't understand.\"\n\nTotal count: 2\n```\n\n#### `implied`\n\nA string specifying a set of default filters to apply when this field is used. The syntax for this string is the same as if the options were given on the command line. The implied filters are only applied if the targeted field does not have any filters set.\n\n```python\nclass Species(ModelBase):\n    name = Text()\n    animal_type = Choice(\n        ['Mammal', 'Fish', 'Bird', 'Reptile', 'Amphibian'],\n        keyname=\"type\",\n        optname=\"type\",\n        realname=\"Type\",\n        inclusive=True,\n    )\n    gestation_period = Number(\n        implied=\"--type Mammal\",\n        optname=\"gp\",\n    )\n\ndef species(options: dict):\n    yield {'name': 'Human', 'type': 'Mammal', 'gestation_period': 280}\n    yield {'name': 'Cat', 'type': 'Mammal', 'gestation_period': 65}\n    yield {'name': 'Eagle', 'type': 'Bird', 'gestation_period': 0}\n    yield {'name': 'Toad', 'type': 'Amphibian'}\n```\n\nThe \"Eagle\" and the \"Toad\" are excluded from the output because the `--gp` option implies `--type Mammal`:\n\n\n```pycon\n>>> Species.cli('--gp \"<100\"', reader=species)\nCat\nType: Mammal\nGestation Period: 65\n\nTotal count: 1\n```\n\n```pycon\n>>> Species.cli('--sort \"gestation period\"', reader=species)\nCat: Mammal. Gestation Period 65.\nHuman: Mammal. Gestation Period 280.\n\nTotal count: 2\n```\n\n```pycon\n>>> Species.cli('--group \"gestation period\"', reader=species)\n[ Gestation Period 65 ]\nCat: Mammal. Gestation Period 65.\n\n[ Gestation Period 280 ]\nHuman: Mammal. Gestation Period 280.\n\nTotal count: 2\n```\n\n```pycon\n>>> Species.cli('--show \"gestation period\"', reader=species)\nHuman: Gestation Period 280.\nCat: Gestation Period 65.\n\nTotal count: 2\n```\n\n```pycon\n>>> Species.cli('--count \"gestation period\"', reader=species)\n\n[ Gestation Period counts ]\n\nGestation Period 280: 1\nGestation Period 65:  1\n\nTotal count: 2\n```\n\nIf `animal_type` is explicitly filtered then the implied `--type` is ignored:\n\n```pycon\n>>> Species.cli('--sort \"gestation period\" --type-isnt Mammal', reader=species)\nToad: Amphibian.\nEagle: Bird. Gestation Period 0.\n\nTotal count: 2\n```\n\n#### `redirect_args`\n\nSet to `True` to redirect all positional arguments to the **first** filter option for this field.\n\n```python\nclass Employee(ModelBase):\n    name = Text(redirect_args=True)\n    title = Text(inclusive=True)\n    gender = Choice([\"Female\", \"Male\", \"Other\"], inclusive=True, default=\"Other\")\n    salary = Number()\n```\n\n```pycon\n>>> Employee.cli('Bob', reader=employees)\nBob Balderson\nTitle: Sales Representative\nGender: Male\nSalary: 2700\n\nTotal count: 1\n```\n\n#### `prefetch`\n\nOptional `Callable` that can potentially alter an `item` before `fetch` gets the field's value from it. Raise `MissingField` exception to skip the item.\n\n```python\ndef can_have_salary(item):\n    if item[\"gender\"] == \"Male\":\n        raise MissingField(\"Sorry boys!\")\n    return item\n\nclass Employee(ModelBase):\n    name = Text(redirect_args=True)\n    title = Text(inclusive=True)\n    gender = Choice([\"Female\", \"Male\", \"Other\"], inclusive=True, default=\"Other\")\n    salary = Number(prefetch=can_have_salary)\n```\n\n```pycon\n>>> Employee.cli('', reader=employees)\nAlice Anderson: Sales Director. Female. Salary 4200.\nBob Balderson: Sales Representative. Male.\nCharlotte Carlson: Sales Representative. Female. Salary 2200.\nTotoro: Company Mascot. Other.\n\nTotal count: 4\n```\n\n#### `styles`\n\nSet the styles with which to display values of this field, as passed on to [click.style](https://click.palletsprojects.com/en/latest/api/#click.style).\n",
    "bugtrack_url": null,
    "license": "Copyright 2023 Petter Nystr\u00f6m  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.",
    "summary": "Framework for CLI applications that filter data streams",
    "version": "0.3.0",
    "project_urls": {
        "Homepage": "https://github.com/jimorie/clicksearch"
    },
    "split_keywords": [
        "click"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ee486d1cfde992c8bb1e95573b61971501f85ef57088d00ec9aecef4ac522877",
                "md5": "5665efb853897bdbb116c8ee1270cc08",
                "sha256": "95bb1b86adb954c49ecf83da0522d279151aaa3f4aced6affaa3cd0ade15c12d"
            },
            "downloads": -1,
            "filename": "clicksearch-0.3.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "5665efb853897bdbb116c8ee1270cc08",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 21268,
            "upload_time": "2024-06-05T10:48:51",
            "upload_time_iso_8601": "2024-06-05T10:48:51.709549Z",
            "url": "https://files.pythonhosted.org/packages/ee/48/6d1cfde992c8bb1e95573b61971501f85ef57088d00ec9aecef4ac522877/clicksearch-0.3.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c2b2d19c66161e9bed7143d2e533afb39fa39174b396fe92bb4bf11dce63f306",
                "md5": "5b094901fb918b0978e7ebc75815e3af",
                "sha256": "0659bb369ac1bd46897b9bf2a46966c0987070f1235366cdb6171215c67e495f"
            },
            "downloads": -1,
            "filename": "clicksearch-0.3.0.tar.gz",
            "has_sig": false,
            "md5_digest": "5b094901fb918b0978e7ebc75815e3af",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 90237,
            "upload_time": "2024-06-05T10:48:53",
            "upload_time_iso_8601": "2024-06-05T10:48:53.910233Z",
            "url": "https://files.pythonhosted.org/packages/c2/b2/d19c66161e9bed7143d2e533afb39fa39174b396fe92bb4bf11dce63f306/clicksearch-0.3.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-06-05 10:48:53",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "jimorie",
    "github_project": "clicksearch",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "clicksearch"
}
        
Elapsed time: 0.31183s