├── .travis.yml ├── CHANGES ├── HowToUseLittletable.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── _config.yml ├── docs └── accessor_pattern_diagrams.md ├── examples ├── Fixed Width data demo.ipynb ├── Littletable PPL demo.ipynb ├── PEPs data demo.ipynb ├── area_codes.py ├── csv_import_examples.py ├── dune_casts.py ├── excel_data_types.py ├── explore_unicode.py ├── fixed_width_demo.py ├── food_data.py ├── food_data_set │ ├── FOOD-DATA-GROUP1.csv │ ├── FOOD-DATA-GROUP2.csv │ ├── FOOD-DATA-GROUP3.csv │ ├── FOOD-DATA-GROUP4.csv │ ├── FOOD-DATA-GROUP5.csv │ ├── README.md │ ├── combined_food_data.csv │ └── food_data_merge.py ├── full_text_search.py ├── littletable_demo.py ├── nfkc_normalization.py ├── peps.json.zip ├── peps.py ├── pivot_demo.py ├── present_groupby_example.py ├── pyscript │ └── matplotlib.html ├── sliced_indexing.py ├── star_trek_tos.py ├── star_trek_tos_eps.csv ├── table_to_dataframe.py ├── time_conversions.py ├── time_zone_db.py ├── unicode_15.1.0.txt.zip ├── unicode_320.txt.zip ├── us_ppl.zip └── zen_of_python_pep20.py ├── how_to_use_littletable.md ├── littletable.py ├── pyproject.toml ├── requirements.dev ├── test ├── abc.csv ├── abc.csv.gz ├── abc.csv.tar.gz ├── abc.csv.xz ├── abc.csv.zip ├── abc.json.gz ├── abc.tar.gz ├── abc.tsv ├── abc.xlsx ├── abc.zip ├── csv_import_http_server.py ├── data_types.xlsx └── star_trek_tos_eps.jsonl ├── tox.ini ├── unit_tests.py └── update_littletable_timestamp.py /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | matrix: 6 | include: 7 | - python: 3.7 8 | - python: 3.8 9 | - python: 3.9 10 | - python: 3.10-dev 11 | dist: xenial 12 | sudo: true 13 | fast_finish: true 14 | 15 | before_install: 16 | - export verint="`python -c "import platform; a,b,c=map(int,platform.python_version().split('.')); print(((a*100+b)*100)+c)"`" 17 | - if [ $verint -ge 30600 ]; then pip install rich; fi 18 | 19 | script: 20 | - python littletable.py 21 | - python unit_tests.py 22 | - PYTHONPATH=. python examples/pivot_demo.py 23 | - PYTHONPATH=. python examples/fixed_width_demo.py 24 | -------------------------------------------------------------------------------- /HowToUseLittletable.txt: -------------------------------------------------------------------------------- 1 | littletable - Usage Notes 2 | ========================= 3 | 4 | (This file is superseded by the Markdown version, https://github.com/ptmcg/littletable/blob/master/how_to_use_littletable.md - refer to that file for more current 5 | information on `littletable`.) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Paul T. McGuire 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include littletable.py 2 | include littletable_demo.py 3 | include pivot_demo.py 4 | include setup.py 5 | include unit_tests.py 6 | include CHANGES 7 | include LICENSE 8 | include README.md 9 | include htmldoc\*.* 10 | include HowToUseLittletable.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # littletable - a Python module to give ORM-like access to a collection of objects 2 | [![Build Status](https://travis-ci.org/ptmcg/littletable.svg?branch=master)](https://travis-ci.org/ptmcg/littletable) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ptmcg/littletable/master) 3 | 4 | - [Introduction](#introduction) 5 | - [Optional dependencies](#optional-dependencies) 6 | - [Importing data from CSV files](#importing-data-from-csv-files) 7 | - [Tabular output](#tabular-output) 8 | - [For More Info](#for-more-info) 9 | - [Sample Demo](#sample-demo) 10 | 11 | Introduction 12 | ------------ 13 | The `littletable` module provides a low-overhead, schema-less, in-memory database access to a collection 14 | of user objects. `littletable` Tables will accept Python `dict`s or any user-defined object type, including: 15 | 16 | - `namedtuples` and `typing.NamedTuples` 17 | - `dataclasses` 18 | - `types.SimpleNamespaces` 19 | - `attrs` classes 20 | - `PyDantic` data models 21 | - `traitlets` 22 | 23 | `littletable` infers the Table's "columns" from those objects' `__dict__`, `__slots__`, or `_fields` mappings to access 24 | object attributes. 25 | 26 | If populated with Python `dict`s, they get stored as `SimpleNamespace`s. 27 | 28 | In addition to basic ORM-style insert/remove/query/delete access to the contents of a `Table`, `littletable` offers: 29 | * simple indexing for improved retrieval performance, and optional enforcing key uniqueness 30 | * access to objects using indexed attributes 31 | * direct import/export to CSV, TSV, JSON, and Excel .xlsx files 32 | * clean tabular output for data presentation 33 | * simplified joins using `"+"` operator syntax between annotated `Table`s 34 | * the result of any query or join is a new first-class `littletable` `Table` 35 | * simple full-text search against multi-word text attributes 36 | * access like a standard Python list to the records in a `Table`, including indexing/slicing, `iter`, `zip`, `len`, `groupby`, etc. 37 | * access like a standard Python `dict` to attributes with a unique index, or like a standard Python `defaultdict(list)` to attributes with a non-unique index 38 | 39 | `littletable` `Table`s do not require an upfront schema definition, but simply work off of the attributes in 40 | the stored values, and those referenced in any query parameters. 41 | 42 | 43 | Optional dependencies 44 | --------------------- 45 | The base `littletable` code has no dependencies outside of the Python stdlib. However, some operations 46 | require additional package installs: 47 | 48 | | operation | additional install required | 49 | |-----------------------------|--------------------------------------------------------------------| 50 | | `Table.present` | `rich` | 51 | | `Table.excel_import/export` | `openpyxl` (plus `defusedxml` or `lxml`, `defusedxml` recommended) | 52 | | `Table.as_dataframe` | `pandas` | 53 | 54 | 55 | Importing data from CSV files 56 | ----------------------------- 57 | You can easily import a CSV file into a `Table` using `Table.csv_import()`: 58 | 59 | ```python 60 | import littletable as lt 61 | t = lt.Table().csv_import("my_data.csv") 62 | # or 63 | t = lt.csv_import("my_data.csv") 64 | ``` 65 | 66 | In place of a local file name, you can also specify an HTTP url: 67 | 68 | ```python 69 | url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/iris.csv" 70 | names = ["sepal-length", "sepal-width", "petal-length", "petal-width", "class"] 71 | iris_table = Table('iris').csv_import(url, fieldnames=names) 72 | ``` 73 | 74 | You can also directly import CSV data as a string: 75 | 76 | ```python 77 | catalog = Table("catalog") 78 | 79 | catalog_data = """\ 80 | sku,description,unitofmeas,unitprice 81 | BRDSD-001,Bird seed,LB,3 82 | BBS-001,Steel BB's,LB,5 83 | MGNT-001,Magnet,EA,8""" 84 | 85 | catalog.csv_import(catalog_data, transforms={'unitprice': int}) 86 | ``` 87 | 88 | Data can also be directly imported from compressed .zip, .gz, and .xz files. 89 | 90 | Files containing JSON-formatted records can be similarly imported using `json_import()`. 91 | 92 | 93 | Tabular output 94 | -------------- 95 | To produce a nice tabular output for a table, you can use the embedded support for 96 | the [rich](https://github.com/willmcgugan/rich) module, `as_html()` in [Jupyter Notebook](https://jupyter.org/), 97 | or the [tabulate](https://github.com/astanin/python-tabulate) module: 98 | 99 | Using `table.present()` (implemented using `rich`; `present()` accepts `rich` `Table` keyword args): 100 | 101 | ```python 102 | table(title_str).present(fields=["col1", "col2", "col3"]) 103 | or 104 | table.select("col1 col2 col3")(title_str).present(caption="caption text", 105 | caption_justify="right") 106 | ``` 107 | 108 | Using `Jupyter Notebook`: 109 | 110 | ```python 111 | from IPython.display import HTML, display 112 | display(HTML(table.as_html())) 113 | ``` 114 | 115 | Using `tabulate`: 116 | 117 | ```python 118 | from tabulate import tabulate 119 | print(tabulate((vars(rec) for rec in table), headers="keys")) 120 | ``` 121 | 122 | For More Info 123 | ------------- 124 | Extended "getting started" notes at [how_to_use_littletable.md](https://github.com/ptmcg/littletable/blob/master/how_to_use_littletable.md). 125 | 126 | Sample Demo 127 | ----------- 128 | Here is a simple `littletable` data storage/retrieval example: 129 | 130 | ```python 131 | from littletable import Table 132 | 133 | customers = Table('customers') 134 | customers.create_index("id", unique=True) 135 | customers.csv_import("""\ 136 | id,name 137 | 0010,George Jetson 138 | 0020,Wile E. Coyote 139 | 0030,Jonny Quest 140 | """) 141 | 142 | catalog = Table('catalog') 143 | catalog.create_index("sku", unique=True) 144 | catalog.insert({"sku": "ANVIL-001", "descr": "1000lb anvil", "unitofmeas": "EA","unitprice": 100}) 145 | catalog.insert({"sku": "BRDSD-001", "descr": "Bird seed", "unitofmeas": "LB","unitprice": 3}) 146 | catalog.insert({"sku": "MAGNT-001", "descr": "Magnet", "unitofmeas": "EA","unitprice": 8}) 147 | catalog.insert({"sku": "MAGLS-001", "descr": "Magnifying glass", "unitofmeas": "EA","unitprice": 12}) 148 | 149 | wishitems = Table('wishitems') 150 | wishitems.create_index("custid") 151 | wishitems.create_index("sku") 152 | 153 | # easy to import CSV data from a string or file 154 | wishitems.csv_import("""\ 155 | custid,sku 156 | 0020,ANVIL-001 157 | 0020,BRDSD-001 158 | 0020,MAGNT-001 159 | 0030,MAGNT-001 160 | 0030,MAGLS-001 161 | """) 162 | 163 | # print a particular customer name 164 | # (unique indexes will return a single item; non-unique 165 | # indexes will return a new Table of all matching items) 166 | print(customers.by.id["0030"].name) 167 | 168 | # see all customer names 169 | for name in customers.all.name: 170 | print(name) 171 | 172 | # print all items sold by the pound 173 | for item in catalog.where(unitofmeas="LB"): 174 | print(item.sku, item.descr) 175 | 176 | # print all items that cost more than 10 177 | for item in catalog.where(lambda o: o.unitprice > 10): 178 | print(item.sku, item.descr, item.unitprice) 179 | 180 | # join tables to create queryable wishlists collection 181 | wishlists = customers.join_on("id") + wishitems.join_on("custid") + catalog.join_on("sku") 182 | 183 | # print all wishlist items with price > 10 (can use Table.gt comparator instead of lambda) 184 | bigticketitems = wishlists().where(unitprice=Table.gt(10)) 185 | for item in bigticketitems: 186 | print(item) 187 | 188 | # list all wishlist items in descending order by price 189 | for item in wishlists().sort("unitprice desc"): 190 | print(item) 191 | 192 | # print output as a nicely-formatted table 193 | wishlists().sort("unitprice desc")("Wishlists").present() 194 | 195 | # print output as an HTML table 196 | print(wishlists().sort("unitprice desc")("Wishlists").as_html()) 197 | 198 | # print output as a Markdown table 199 | print(wishlists().sort("unitprice desc")("Wishlists").as_markdown()) 200 | 201 | ``` 202 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/accessor_pattern_diagrams.md: -------------------------------------------------------------------------------- 1 | Accessor classes 2 | 3 | ```mermaid 4 | classDiagram 5 | 6 | class Object { 7 | attribute 8 | access()* 9 | } 10 | 11 | class Accessor { 12 | object 13 | __getattr__(attrname) 14 | } 15 | 16 | class ObjectAttributeWrapper { 17 | object 18 | object_attribute 19 | action()* 20 | } 21 | 22 | Accessor --> Object 23 | Object ..> Accessor: access (creates) 24 | Accessor ..> ObjectAttributeWrapper: __getattr__ (creates) 25 | ObjectAttributeWrapper --> Object 26 | 27 | 28 | ``` 29 | 30 | ```mermaid 31 | sequenceDiagram 32 | actor client as client
33 | participant tbl as catalog:
Table 34 | participant acc as accessor:
IndexAccessor 35 | participant w as wrapper:
IndexWrapper 36 | 37 | note over client,w: create index on "sku" 38 | 39 | client ->> tbl: create_index("sku", unique=True) 40 | note over tbl: build "sku" index dict
mapping obj.sku -> obj
for all obj in tbl 41 | tbl ->> tbl: index =
{getattr(obj, "sku"): obj
for obj in self.obj_list} 42 | tbl ->> tbl: self.indexes["sku"] = index 43 | 44 | 45 | note over client,w: catalog.by.sku["001"] 46 | 47 | note right of client: get "by" accessor 48 | client ->> tbl: by 49 | tbl ->> acc: create(catalog) 50 | tbl -->> client: accessor 51 | 52 | note right of client: get "sku" index wrapper 53 | client ->> acc: __getattr__("sku") 54 | acc ->> w: create(idx=catalog.indexes["sku"]) 55 | w ->> w: self.index = idx 56 | acc -->> client: wrapper 57 | 58 | note right of client: get "001" element in index 59 | client ->> w: __getitem__("001") 60 | w ->> w: self.index["001"] 61 | w -->> client: ["001"] object or KeyError 62 | 63 | ``` -------------------------------------------------------------------------------- /examples/Fixed Width data demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import littletable as lt\n", 12 | "from IPython.display import display, HTML\n", 13 | "_HTML = lambda tbl, fields='*': display(HTML(tbl.as_html(fields)))" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "metadata": { 20 | "collapsed": true 21 | }, 22 | "outputs": [], 23 | "source": [ 24 | "data = \"\"\"\\\n", 25 | "0010GEORGE JETSON 12345 SPACESHIP ST HOUSTON TX 4.9\n", 26 | "0020WILE E COYOTE 312 ACME BLVD TUCSON AZ 7.3\n", 27 | "0030FRED FLINTSTONE 246 GRANITE LANE BEDROCK CA 2.6\n", 28 | "0040JONNY QUEST 31416 SCIENCE AVE PALO ALTO CA 8.1\n", 29 | "\"\"\"\n", 30 | "\n", 31 | "columns = [\n", 32 | " (\"id_no\", 0, ),\n", 33 | " (\"name\", 4, ),\n", 34 | " (\"address\", 21, ),\n", 35 | " (\"city\", 42, ),\n", 36 | " (\"state\", 56, 58, ),\n", 37 | " (\"tech_skill_score\", 59, None, float),\n", 38 | " ]\n", 39 | "\n", 40 | "reader = lt.FixedWidthReader(columns, data)" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": { 47 | "collapsed": false 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "characters_table = lt.Table().insert_many(reader)" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 4, 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "outputs": [ 61 | { 62 | "data": { 63 | "text/html": [ 64 | "\n", 65 | "\n", 66 | "\n", 67 | "\n", 68 | "\n", 69 | "\n", 70 | "
id_no
name
address
city
state
tech_skill_score
0010
GEORGE JETSON
12345 SPACESHIP ST
HOUSTON
TX
4.9
0020
WILE E COYOTE
312 ACME BLVD
TUCSON
AZ
7.3
0030
FRED FLINTSTONE
246 GRANITE LANE
BEDROCK
CA
2.6
0040
JONNY QUEST
31416 SCIENCE AVE
PALO ALTO
CA
8.1
" 71 | ], 72 | "text/plain": [ 73 | "" 74 | ] 75 | }, 76 | "metadata": {}, 77 | "output_type": "display_data" 78 | } 79 | ], 80 | "source": [ 81 | "_HTML(characters_table, \"id_no name address city state *\")" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": { 88 | "collapsed": true 89 | }, 90 | "outputs": [], 91 | "source": [] 92 | } 93 | ], 94 | "metadata": { 95 | "kernelspec": { 96 | "display_name": "Python 3", 97 | "language": "python", 98 | "name": "python3" 99 | }, 100 | "language_info": { 101 | "codemirror_mode": { 102 | "name": "ipython", 103 | "version": 3 104 | }, 105 | "file_extension": ".py", 106 | "mimetype": "text/x-python", 107 | "name": "python", 108 | "nbconvert_exporter": "python", 109 | "pygments_lexer": "ipython3", 110 | "version": "3.5.2" 111 | } 112 | }, 113 | "nbformat": 4, 114 | "nbformat_minor": 2 115 | } 116 | -------------------------------------------------------------------------------- /examples/area_codes.py: -------------------------------------------------------------------------------- 1 | """ 2 | What do telephone cords have to do with area codes? 3 | 4 | Telephone cords always had 4 wires: red, green, black, and white. But for 5 | most house and office phones back in the day, all you needed to connect were 6 | the red and green ones. 7 | 8 | In analog times, "dialing" each number involved opening and closing the circuit 9 | N number of times (to dial a "0" required 10 circuit open/closes). Rotary phones 10 | did this for you, but you could do it yourself by tapping the wire on the 11 | mounting screw (I think this was actually used as an escape in a 70's era spy 12 | movie or TV show). 13 | 14 | Phone exchanges did the same thing with physical switches to open and close 15 | circuits. When area codes were added, their 3-digit pattern had to be detectable 16 | as different from regular phone numbers. The middle number had to be a 0 or 1, 17 | and the first and third numbers had to be digits 2-9. Since the switches were 18 | physical, area codes were assigned by number and activity of phones, so that the 19 | most phone-active areas used the lowest possible numbers for the fewest number 20 | of circuit open/close sequences. So who got 212? New York City (Manhattan). 21 | 213? Los Angeles. 312? Chicago. 214? Dallas. 412? Pittsburgh. 22 | 23 | 1947 map of initial area codes: 24 | https://upload.wikimedia.org/wikipedia/en/thumb/7/72/North_American_Numbering_Plan_NPA_map_BTM_1947.png/1400px-North_American_Numbering_Plan_NPA_map_BTM_1947.png 25 | 26 | Now that the phone system is completely digital, these area code constraints 27 | have largely been lifted (still no area codes start with 0 or 1, and the x00 and 28 | x11 codes are reserved for special uses). 29 | """ 30 | import littletable as lt 31 | from typing import Union 32 | 33 | try: 34 | area_codes = lt.csv_import( 35 | "area_codes.csv", 36 | transforms = {"latitude": float, "longitude": float}, 37 | ) 38 | except FileNotFoundError: 39 | area_codes = lt.csv_import( 40 | "https://raw.githubusercontent.com/ravisorg/Area-Code-Geolocation-Database/refs/heads/master/us-area-code-cities.csv", 41 | fieldnames = "area_code,city,state,country,latitude,longitude".split(","), 42 | transforms = {"latitude": float, "longitude": float}, 43 | ) 44 | area_codes.csv_export("area_codes.csv") 45 | 46 | # collapse duplicate entries for the same area code 47 | area_codes = area_codes.unique("area_code") 48 | print(len(area_codes)) 49 | 50 | 51 | class AreaCode: 52 | def __init__(self, digits: Union[tuple[int, int, int], str]): 53 | if isinstance(digits, str): 54 | digits = tuple(int(d) for d in digits) 55 | self.digits: tuple[int, int, int] = digits 56 | 57 | @property 58 | def is_analog(self): 59 | return self.digits[1] in (0, 1) and not set(self.digits[::2]) & {0, 1} 60 | 61 | def __len__(self): 62 | return sum(self.digits) + (10 * (self.digits.count(0))) 63 | 64 | def __str__(self): 65 | return "".join(str(d) for d in self.digits) 66 | 67 | 68 | area_codes.compute_field("area_code", lambda rec: AreaCode(rec.area_code)) 69 | 70 | # analog area codes must start and end with digit 2-9, and have middle digit of 0 or 1 71 | analog_codes: lt.Table = area_codes.where(lambda rec: rec.area_code.is_analog) 72 | print(len(analog_codes)) 73 | 74 | # find those area codes that require the fewest clicks 75 | analog_codes.sort(key=lambda rec: len(rec.area_code)) 76 | 77 | analog_codes.compute_field("clicks", lambda rec: len(rec.area_code)) 78 | analog_codes.where(clicks=lt.Table.le(14)).present(width=120) 79 | -------------------------------------------------------------------------------- /examples/csv_import_examples.py: -------------------------------------------------------------------------------- 1 | # 2 | # csv_import_examples.py 3 | # 4 | # Some examples of importing data from CSV sources. 5 | # 6 | # CSV data may be imported from: 7 | # - a Python string 8 | # - a local .csv file 9 | # - a .zip file containing a .csv file 10 | # - a .gz file containing a .csv file 11 | # - a .xz or .lzma file containing a .csv file 12 | # - an http or https URL to a page containing raw CSV data 13 | # 14 | # littletable tables can also import data from: 15 | # - TSV files (tab-separated values) 16 | # - JSON files 17 | # . a single JSON list of objects, or 18 | # . multiple JSON objects 19 | # - Excel spreadsheets 20 | # 21 | 22 | import littletable as lt 23 | 24 | # read from CSV data in a string 25 | catalog_data = """\ 26 | sku,descr,unitofmeas,unitprice 27 | BRDSD-001,Bird seed,LB,3 28 | BBS-001,Steel BB's,LB,5 29 | MGNT-001,Magnet,EA,8 30 | MAGLS-001,Magnifying glass,EA,12 31 | ANVIL-001,1000lb anvil,EA,100 32 | ROPE-001,1 in. heavy rope,100FT,10 33 | ROBOT-001,Domestic robot,EA,5000""" 34 | catalog = lt.csv_import(catalog_data, transforms={"unitprice": int}) 35 | 36 | # read from a CSV file 37 | data = lt.csv_import("my_data.csv") 38 | 39 | # read from a ZIP file containing a single CSV file 40 | data = lt.csv_import("my_data.csv.zip") 41 | 42 | # read from a GZIP file containing a single CSV file 43 | data = lt.csv_import("my_data.csv.gz") 44 | 45 | # read from CSV data in a remote URL 46 | url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/iris.csv" 47 | names = ["sepal-length", "sepal-width", "petal-length", "petal-width", "class"] 48 | iris_transforms = dict.fromkeys( 49 | ["petal-length", "petal-width", "sepal-length", "sepal-width"], float 50 | ) 51 | iris_table = lt.csv_import( 52 | url, fieldnames=names, transforms=iris_transforms 53 | )("iris") 54 | print(iris_table.info()) 55 | 56 | # accumulate data from multiple CSVs into a single table, and then resave as a single CSV 57 | from pathlib import Path 58 | csv_files = Path(".").glob("*.csv") 59 | 60 | all_data = lt.Table() 61 | for csv in csv_files: 62 | # repeated calls to csv_import appends data to an existing table 63 | all_data.csv_import(csv) 64 | 65 | all_data.csv_export("accumulated_data.csv") 66 | -------------------------------------------------------------------------------- /examples/dune_casts.py: -------------------------------------------------------------------------------- 1 | # 2 | # dune_casts.py 3 | # 4 | # Use join() to create a consolidated table of the casts of Dune, 5 | # from the 1984 movie, the 2000 mini-series, and the 2021 movie. 6 | # 7 | import littletable as lt 8 | from operator import attrgetter 9 | 10 | dune_casts_csv = """\ 11 | character,actor,year 12 | Lady Jessica Atreides,Francesca Annis,1984 13 | Piter De Vries,Brad Dourif,1984 14 | Padishah Emperor Shaddam IV,José Ferrer,1984 15 | Shadout Mapes,Linda Hunt,1984 16 | Thufir Hawat,Freddie Jones,1984 17 | Duncan Idaho,Richard Jordan,1984 18 | Paul Atreides,Kyle MacLachlan,1984 19 | Princess Irulan,Virginia Madsen,1984 20 | Reverend Mother Ramallo,Silvana Mangano,1984 21 | Stilgar,Everett McGill,1984 22 | Baron Vladimir Harkonnen,Kenneth McMillan,1984 23 | Reverend Mother Gaius Helen Mohiam,Siân Phillips,1984 24 | Duke Leto Atreides,Jürgen Prochnow,1984 25 | Glossu Beast Rabban,Paul L. Smith,1984 26 | Gurney Halleck,Patrick Stewart,1984 27 | Feyd-Rautha Harkonnen,Sting,1984 28 | Dr. Yueh,Dean Stockwell,1984 29 | Dr. Liet-Kynes,Max von Sydow,1984 30 | Alia Atreides,Alicia Witt,1984 31 | Chani,Sean Young,1984 32 | Otheym,Honorato Magaloni,1984 33 | Jamis,Judd Omen,1984 34 | Harah,Molly Wryn,1984 35 | character,actor 36 | Duke Leto Atreides,William Hurt,2000 37 | Paul Atreides,Alec Newman,2000 38 | Lady Jessica Atreides,Saskia Reeves,2000 39 | Gurney Halleck,P.H. Moriarty,2000 40 | Baron Vladimir Harkonnen,Ian McNeice,2000 41 | Feyd-Rautha Harkonnen,Matt Keeslar,2000 42 | Glossu Beast Rabban,László I. Kish,2000 43 | Padishah Emperor Shaddam IV,Giancarlo Giannini,2000 44 | Princess Irulan,Julie Cox,2000 45 | Stilgar,Uwe Ochsenknecht,2000 46 | Reverend Mother Gaius Helen Mohiam,Zuzana Geislerová,2000 47 | Alia Atreides,Laura Burton,2000 48 | Duncan Idaho,James Watson,2000 49 | Chani,Barbora Kodetová,2000 50 | Otheym,Jakob Schwarz,2000 51 | Dr. Liet-Kynes,Karel Dobrý,2000 52 | Thufir Hawat,Jan Vlasák,2000 53 | Dr. Yueh,Robert Russell,2000 54 | Piter De Vries,Jan Unger,2000 55 | Jamis,Christopher Lee Brown,2000 56 | Shadout Mapes,Jaroslava Siktancova,2000 57 | Reverend Mother Ramallo,Drahomíra Fialková,2000 58 | Young Mother Ramallo,Petra Spindlerová,2000 59 | Harah,...,2000 60 | Duncan Idaho,Jason Momoa,2021 61 | Paul Atreides,Timothée Chalamet,2021 62 | Chani,Zendaya,2021 63 | Lady Jessica Atreides,Rebecca Ferguson,2021 64 | Gurney Halleck,Josh Brolin,2021 65 | Glossu Beast Rabban,Dave Bautista,2021 66 | Duke Leto Atreides,Oscar Isaac,2021 67 | Stilgar,Javier Bardem,2021 68 | Piter De Vries,David Dastmalchian,2021 69 | Baron Vladimir Harkonnen,Stellan Skarsgård,2021 70 | Reverend Mother Gaius Helen Mohiam,Charlotte Rampling,2021 71 | Thufir Hawat,Stephen McKinley Henderson,2021 72 | Dr. Yueh,Chen Chang,2021 73 | Dr. Liet-Kynes,Sharon Duncan-Brewster,2021 74 | Jamis,Babs Olusanmokun,2021 75 | Harah,Gloria Obianyo,2021 76 | Padishah Emperor Shaddam IV,Christopher Walken,2021 77 | Shadout Mapes,Golda Rosheuvel,2021 78 | Princess Irulan,Florence Pugh,2021 79 | Reverend Mother Ramallo,Giusi Merli,2021 80 | Feyd-Rautha Harkonnen,Austin Butler,2021 81 | Alia Atreides,Anya Taylor-Joy,2021 82 | Otheym,...,2021 83 | """ 84 | 85 | dune_casts = lt.Table("dune_1984").csv_import(dune_casts_csv).create_index("character") 86 | dune_1984 = dune_casts.where(year="1984").compute_field("actor_1984", attrgetter("actor")) 87 | dune_2000 = dune_casts.where(year="2000").compute_field("actor_2000", attrgetter("actor")) 88 | dune_2021 = dune_casts.where(year="2021").compute_field("actor_2021", attrgetter("actor")) 89 | 90 | join = dune_1984.join_on("character") + dune_2000 + dune_2021 91 | dune_combined = join()("Dune Casts (combined)") 92 | dune_combined.present( 93 | fields=[ 94 | "character", 95 | ("actor_1984", {"header": "Actor (1984)"}), 96 | ("actor_2000", {"header": "Actor (2000)"}), 97 | ("actor_2021", {"header": "Actor (2021)"}), 98 | ] 99 | ) 100 | 101 | print() 102 | print(dune_combined.select("character actor_1984 actor_2000 actor_2021").json_export()) 103 | print(len(dune_combined)) -------------------------------------------------------------------------------- /examples/excel_data_types.py: -------------------------------------------------------------------------------- 1 | # 2 | # excel_data_types.py 3 | # 4 | # Demonstrate data type conversions done automatically when importing from Excel 5 | # 6 | import littletable as lt 7 | 8 | xl = lt.Table().excel_import("../test/data_types.xlsx") 9 | 10 | xl.present() 11 | for row in xl: 12 | print(row.name, repr(row.value), type(row.value), row.type) 13 | -------------------------------------------------------------------------------- /examples/explore_unicode.py: -------------------------------------------------------------------------------- 1 | # explore_unicode.py 2 | # 3 | # Read and process the standard Unicode table (from http://www.unicode.org/L2/L1999/UnicodeData.html) 4 | # 5 | # Field Title Normative/Informative Desc 6 | # ────────────────────────────────────────────────────────────────────────────────────────────────────── 7 | # 0 Code value normative Code value in 4-digit 8 | # hexadecimal format. 9 | # 1 Character name normative These names match exactly the 10 | # names published in Chapter 7 of 11 | # the Unicode Standard, Version 12 | # 2.0, except for the two 13 | # additional characters. 14 | # 2 General category normative / informative This is a useful breakdown into 15 | # various "character types" which 16 | # can be used as a default 17 | # categorization in 18 | # implementations. See below for 19 | # a brief explanation. 20 | # 3 Canonical combining classes normative The classes used for the 21 | # Canonical Ordering Algorithm in 22 | # the Unicode Standard. These 23 | # classes are also printed in 24 | # Chapter 4 of the Unicode 25 | # Standard. 26 | # 4 Bidirectional category normative See the list below for an 27 | # explanation of the 28 | # abbreviations used in this 29 | # field. These are the categories 30 | # required by the Bidirectional 31 | # Behavior Algorithm in the 32 | # Unicode Standard. These 33 | # categories are summarized in 34 | # Chapter 3 of the Unicode 35 | # Standard. 36 | # 5 Character decomposition mapping normative In the Unicode Standard, not 37 | # all of the mappings are full 38 | # (maximal) decompositions. 39 | # Recursive application of 40 | # look-up for decompositions 41 | # will, in all cases, lead to a 42 | # maximal decomposition. The 43 | # decomposition mappings match 44 | # exactly the decomposition 45 | # mappings published with the 46 | # character names in the Unicode 47 | # Standard. 48 | # 6 Decimal digit value normative This is a numeric field. If the 49 | # character has the decimal digit 50 | # property, as specified in 51 | # Chapter 4 of the Unicode 52 | # Standard, the value of that 53 | # digit is represented with an 54 | # integer value in this field 55 | # 7 Digit value normative This is a numeric field. If the 56 | # character represents a digit, 57 | # not necessarily a decimal 58 | # digit, the value is here. This 59 | # covers digits which do not form 60 | # decimal radix forms, such as 61 | # the compatibility superscript 62 | # digits 63 | # 8 Numeric value normative This is a numeric field. If the 64 | # character has the numeric 65 | # property, as specified in 66 | # Chapter 4 of the Unicode 67 | # Standard, the value of that 68 | # character is represented with 69 | # an integer or rational number 70 | # in this field. This includes 71 | # fractions as, e.g., "1/5" for 72 | # U+2155 VULGAR FRACTION ONE 73 | # FIFTH Also included are 74 | # numerical values for 75 | # compatibility characters such 76 | # as circled numbers. 77 | # 9 Mirrored normative If the character has been 78 | # identified as a "mirrored" 79 | # character in bidirectional 80 | # text, this field has the value 81 | # "Y"; otherwise "N". The list of 82 | # mirrored characters is also 83 | # printed in Chapter 4 of the 84 | # Unicode Standard. 85 | # 10 Unicode 1.0 Name informative This is the old name as 86 | # published in Unicode 1.0. This 87 | # name is only provided when it 88 | # is significantly different from 89 | # the Unicode 3.0 name for the 90 | # character. 91 | # 11 10646 comment field informative This is the ISO 10646 comment 92 | # field. It is in parantheses in 93 | # the 10646 names list. 94 | # 12 Uppercase mapping informative Upper case equivalent mapping. 95 | # If a character is part of an 96 | # alphabet with case 97 | # distinctions, and has an upper 98 | # case equivalent, then the upper 99 | # case equivalent is in this 100 | # field. See the explanation 101 | # below on case distinctions. 102 | # These mappings are always 103 | # one-to-one, not one-to-many or 104 | # many-to-one. This field is 105 | # informative. 106 | # 13 Lowercase mapping informative Similar to Uppercase mapping 107 | # 14 Titlecase mapping informative Similar to Uppercase mapping 108 | # """ 109 | # 110 | import re 111 | import littletable as lt 112 | 113 | 114 | fieldnames = [ 115 | "code_value_hex", 116 | "name", 117 | "category", 118 | "combining_classes", 119 | "bidirectional_category", 120 | "char_decomposition_mapping", 121 | "decimal_digit_value", 122 | "digit_value", 123 | "numeric_value", 124 | "mirrored", 125 | "unicode_1_name", 126 | "iso10646_comment", 127 | "uppercase_hex", 128 | "lowercase_hex", 129 | "titlecase_hex", 130 | ] 131 | unicode_url = "https://www.unicode.org/Public/15.1.0/ucd/UnicodeData.txt" 132 | unicode_file = "unicode_15.1.0.txt.zip" 133 | unicode = lt.Table().csv_import( 134 | # unicode_url, 135 | unicode_file, 136 | delimiter=";", 137 | transforms={ 138 | "decimal_digit_value": int, 139 | "digit_value": int, 140 | "numeric_value": int, 141 | }, 142 | fieldnames=fieldnames, 143 | ) 144 | 145 | # remove surrogates 146 | unicode.delete(category="Cs") 147 | 148 | unicode.compute_field("code_value", lambda r: int(r.code_value_hex, 16)) 149 | unicode.compute_field("uppercase_int", lambda r: int(r.uppercase_hex, 16)) 150 | unicode.compute_field("lowercase_int", lambda r: int(r.lowercase_hex, 16)) 151 | unicode.compute_field("titlecase_int", lambda r: int(r.titlecase_hex, 16)) 152 | unicode.compute_field("character", lambda r: chr(r.code_value)) 153 | unicode.compute_field("uppercase", lambda r: chr(r.uppercase_int)) 154 | unicode.compute_field("lowercase", lambda r: chr(r.lowercase_int)) 155 | unicode.compute_field("titlecase", lambda r: chr(r.titlecase_int)) 156 | unicode.compute_field("is_identifier", lambda r: r.character.isidentifier()) 157 | 158 | unicode.create_index("code_value_hex", unique=True) 159 | unicode.create_index("code_value", unique=True) 160 | 161 | # 162 | # Explore some interesting groups of symbols in the Unicode set 163 | # 164 | def present_table( 165 | title: str, source_table: lt.Table = unicode 166 | ) -> lt.Table: 167 | """ 168 | Function to search for Unicode characters that match a starting string, and 169 | presents a table showing name, character, and decimal code value 170 | """ 171 | tbl = source_table.select("name character code_value code_value_hex") 172 | tbl.present( 173 | caption="Total {} symbols".format(len(tbl)), 174 | caption_justify="left", 175 | ) 176 | return tbl 177 | 178 | def present_symbol_group( 179 | start_str: str, title: str, source_table: lt.Table = unicode 180 | ) -> lt.Table: 181 | """ 182 | Function to search for Unicode characters that match a starting string, and 183 | presents a table showing name, character, and decimal code value 184 | """ 185 | tbl = source_table.where(name=lt.Table.startswith(start_str))(title) 186 | return present_table(title, tbl) 187 | 188 | def present_symbol_search_result( 189 | query: str, title: str, source_table: lt.Table = unicode 190 | ) -> lt.Table: 191 | """ 192 | Function to search for Unicode characters that match a search query, and 193 | presents a table showing name, character, and decimal code value 194 | """ 195 | search_result = source_table.search.name_words(query,as_table=True)(title) 196 | return present_table(title, search_result) 197 | 198 | 199 | # display the characters of the I Ching 200 | i_ching = present_symbol_group("HEXAGRAM FOR", "I Ching") 201 | 202 | # display the characters of the Tai Xuan Jing 203 | tai_xuan_jing = present_symbol_group("TETRAGRAM FOR", "Tai Xuan Jing") 204 | 205 | # display all the Roman numerals 206 | numerics = unicode.where(numeric_value=lt.Table.ne(None)).orderby("numeric_value") 207 | roman_numerals = present_symbol_group("ROMAN NUMERAL", "Roman Numerals", numerics) 208 | 209 | # display all Braille characters 210 | braille = present_symbol_group("BRAILLE PATTERN", "Braille") 211 | 212 | # display all Box Drawing characters 213 | box_drawing = present_symbol_group( 214 | "BOX DRAWINGS", "Box Drawing", unicode.where(code_value=lt.Table.lt(10000)) 215 | ) 216 | 217 | # clock faces 218 | clock_faces = present_symbol_group("CLOCK FACE", "Clock Faces") 219 | 220 | # die faces 221 | die_faces = present_symbol_group("DIE FACE", "Die Faces") 222 | 223 | # create search index for random search for words in code point names or comments 224 | unicode.create_search_index("name_words", using="name unicode_1_name iso10646_comment") 225 | 226 | dots = present_symbol_search_result("DOT", "Dots") 227 | 228 | arrows = present_symbol_search_result("ARROW", "Arrows") 229 | 230 | faces = present_symbol_search_result("FACE", "Faces") 231 | 232 | # chess pieces 233 | chess_pieces = present_symbol_search_result( 234 | r"white black ++chess --rotated --neutral --turned", 235 | "Chess Pieces", 236 | ) 237 | -------------------------------------------------------------------------------- /examples/fixed_width_demo.py: -------------------------------------------------------------------------------- 1 | import littletable as lt 2 | 3 | data = """\ 4 | 0010GEORGE JETSON 12345 SPACESHIP ST HOUSTON TX 4.9 5 | 0020WILE E COYOTE 312 ACME BLVD TUCSON AZ 7.3 6 | 0030FRED FLINTSTONE 246 GRANITE LANE BEDROCK CA 2.6 7 | 0040JONNY QUEST 31416 SCIENCE AVE PALO ALTO CA 8.1 8 | """ 9 | 10 | columns = [ 11 | ("id_no", 0, ), 12 | ("name", 4, ), 13 | ("address", 21, ), 14 | ("city", 42, ), 15 | ("state", 56, 58, ), 16 | ("tech_skill_score", 59, None, float), 17 | ] 18 | 19 | characters_table = lt.Table().insert_many( 20 | lt.FixedWidthReader(columns, data) 21 | ) 22 | 23 | print(len(characters_table)) 24 | print(characters_table[0]) 25 | print(max(characters_table, key=lambda rec: rec.tech_skill_score)) -------------------------------------------------------------------------------- /examples/food_data.py: -------------------------------------------------------------------------------- 1 | # 2 | # food_data.py 3 | # 4 | # Work with food data to do some Table functions. 5 | # (Using present() requires installation of the rich module.) 6 | # 7 | from pathlib import Path 8 | 9 | import littletable as lt 10 | 11 | data_dir = Path("food_data_set") 12 | food_data: lt.Table = lt.csv_import( 13 | data_dir / "combined_food_data.csv", 14 | transforms={"*": lt.Table.convert_numeric} 15 | )("Food Data") 16 | 17 | # create some useful indexes 18 | food_data.create_index("id", unique=True) 19 | food_data.create_search_index("food") 20 | 21 | # find foods that have "cheese" in the name, but not "pizza" or "bread" 22 | search_query = "cheese --pizza --bread" 23 | food_data.search.food(search_query).orderby("id").select("id food caloric_value").present() 24 | 25 | # 10 foods with the highest caloric value 26 | food_data.orderby("caloric_value desc")[:10].select("id food caloric_value").present() 27 | -------------------------------------------------------------------------------- /examples/food_data_set/FOOD-DATA-GROUP4.csv: -------------------------------------------------------------------------------- 1 | ,Unnamed: 0,food,Caloric Value,Fat,Saturated Fats,Monounsaturated Fats,Polyunsaturated Fats,Carbohydrates,Sugars,Protein,Dietary Fiber,Cholesterol,Sodium,Water,Vitamin A,Vitamin B1,Vitamin B11,Vitamin B12,Vitamin B2,Vitamin B3,Vitamin B5,Vitamin B6,Vitamin C,Vitamin D,Vitamin E,Vitamin K,Calcium,Copper,Iron,Magnesium,Manganese,Phosphorus,Potassium,Selenium,Zinc,Nutrition Density 2 | 0,0,chocolate pudding fat free,105,0.3,0,0,0,23.6,17.8,2.2,0.3,1.1,0.1,86.1,0,0.021,0.060,0.069,0.046,0.022,0.2,0.084,0,0,0,0.098,44.1,0.035,1.9,17,0.040,61,235,0.052,0.3,72.4 3 | 1,1,tapioca pudding,143,4.3,1.1,2.8,0.088,23.9,16.4,2.1,0,1.1,0.2,79,0,0.025,0.003,0.021,0.1,0.031,0.3,0.052,0.3,0,0.2,0.010,78.1,0.026,0.1,6.6,0.096,66,101.2,0,0.2,108.8 4 | 2,2,tapioca pudding fat free,105,0.4,0.1,0.080,0.067,23.9,15.9,1.6,0,1.1,0.2,85.4,0,0.044,0.096,0.012,0.043,0.1,0.056,0.039,0.3,0,0.072,0,58.2,0.004,0.1,5.6,0.023,73.9,78.4,0,0.2,84.5 5 | 3,3,rice pudding,122,2.4,1.4,0.6,0.1,20.8,13.1,3.6,0.3,13.6,0.1,84.9,0.043,0.002,0.098,0.089,0.098,0.2,0.2,0.4,0.074,1.5,0,0.011,0.063,107.4,0.014,0.1,9,0.1,92.7,141.3,0.083,27.329 6 | 4,4,corn pudding,328,12.6,6.3,3.9,1.4,42.4,16.5,11.1,3,180,0.7,180.8,0.2,0.1,0.2,0.074,0.026,0.4,2.6,1.3,0.3,9.3,0.048,0.7,0.066,97.5,0.1,1.3,37.5,0.2,225,440,0.069,69.795 7 | 5,5,chocolate pudding,213,6.9,1.9,4.1,0.2,34.5,25.8,3.1,0,1.5,0.2,104.2,0.045,0.003,0.061,0.044,0.1,0.2,0.4,0.062,0.5,0,0.5,0.066,76.5,0.1,1.9,27,0.2,84,276,0,0.5,123.424 8 | 6,6,lemon pudding,173,4.4,2.6,1.3,0.2,30.2,0,4.1,0,16.5,0.4,109.8,0.026,0.092,0.071,0.028,0.2,0.1,0.4,0.083,1.2,0,0,0,148.5,0.070,0.058,16.5,0.007,307.5,190.5,0.041,0.5,188.516 9 | 7,7,vanilla pudding,195,5.7,1.5,3.4,0.1,33.9,25.5,2.2,0,1.5,0.3,107.5,0.018,0.012,0.084,0.092,0.1,0.047,0.2,0.048,0.3,0,0.5,0.034,73.5,0.080,0.1,6,0.031,61.5,97.5,0,0.2,115.796 10 | 8,8,profeel proteiinirahka valio,140,0.4,0.2,16.8,16.8,19.3,0.2,175,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,194.7 11 | 9,9,cottage cheese creamed,20,0.9,0.3,0.2,0.084,0.7,0.5,2.2,0,3.4,0.095,16,0.022,0.089,0.039,0.060,0.060,0.059,0.1,0.036,0,0.085,0.082,0,16.6,0.019,0.071,1.6,0.009,31.8,20.8,0.045,0.018,20.576 12 | 10,10,zaziki milfina,51,4.4,1.6,1.2,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.4 13 | 11,11,cottage cheese low fat,163,2.3,1.5,0.7,0.041,6.1,6.1,28,0,9,0.9,186.4,0.021,0.081,0.005,0.072,0.4,0.3,0.5,0.2,0,0,0.035,0.024,137.9,0.005,0.3,11.3,0.041,302.8,194.4,0.055,0.9,174.683 14 | 12,12,cottage cheese nonfat,104,0.4,0.2,0.1,0.066,9.7,2.7,15,0,10.2,0.5,117.5,0.093,0.063,0.013,0.006,0.3,0.2,0.6,0.039,0,0,0.096,0,124.7,0.071,0.2,16,0.063,275.5,198.7,0.063,0.7,150.072 15 | 13,13,vanilla yogurt low fat,208,3.1,2,0.8,0.036,33.8,33.8,12.1,0,12.3,0.2,193.6,0.051,0.1,0.054,0.010,0.5,0.3,1.4,0.1,2,0,0.029,0.024,419,0.012,0.2,39.2,0.023,330.8,536.6,0.049,2,470.265 16 | 14,14,classique yogurt plain 2 liberte,30,1.2,0.7,0.7,0,1.7,1.7,3.3,0,0.017,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,6.2 17 | 15,15,yoplait light strawberry yogurt yoplait,79,0,14.1,8.8,4.4,0.074,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.061 18 | 16,16,vanilla yogurt,145,2.1,1.3,0.5,0.069,23.5,23.5,8.4,0,8.5,0.1,134.3,0.005,0.091,0.068,0.026,0.3,0.2,0.9,0.094,1.4,0,0.003,0.037,290.7,0.089,0.1,27.2,0.087,229.5,372.3,0.080,1.4,326.243 19 | 17,17,chocolate yogurt,168,0,0,0,0,35.3,22.5,5.3,1.8,1.5,0.2,107.4,0,0.083,0.019,0.042,0.3,0.3,0,0.050,0,0,0,0,132,0.3,0.6,60,0,249,508.5,0.069,1.7,175.0 20 | 18,18,yogurt parfait,125,1.5,0.8,0.5,0.2,23.6,17.4,5,1.6,4.5,0.048,117.7,0.073,0.1,0.018,0.100,0.3,1.1,0,0.3,21.3,0.042,0.4,0.085,156.5,0.025,0.7,25.3,0,138.6,281.6,0.088,1.3,210.272 21 | 19,19,yogurt,92,4.9,3.1,1.3,0.1,7,7,5.2,0,19.5,0.005,131.9,0.026,0.017,0.060,0.081,0.2,0.1,0.6,0.026,0.8,0.076,0.035,0.020,181.5,0.049,0.001,18,0.084,142.5,232.5,0.075,0.9,199.51 22 | 20,20,chocolate frozen yogurt,221,6.3,4,1.7,0.2,37.6,33.5,5.2,4,22.6,0.1,123.9,0.036,0.080,0.008,0.088,0.3,0.2,0,0.021,0,0.057,0.2,0.071,174,0.028,0.8,43.5,0,154.9,407.2,0.059,0.5,227.929 23 | 21,21,frozen yogurt,221,6.3,4,1.7,0.2,37.6,34.7,5.2,0,22.6,0.1,123.9,0.013,0.062,0.095,0.050,0.3,0.1,0,0.072,1.2,0.010,0.2,0.054,174,0.074,0.8,17.4,0,154.9,271.4,0.005,0.5,225.163 24 | 22,22,yogurt low fat,154,3.8,2.5,1,0.1,17.2,17.2,12.9,0,14.7,0.2,208.4,0.021,0.1,0.045,0.034,0.5,0.3,1.4,0.1,2,0,0.007,0.007,448.4,0.005,0.2,41.7,0.039,352.8,573.3,0.071,2.2,484.517 25 | 23,23,vanilla yoghurt oikos,120,1.8,24,4.9,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.8 26 | 24,24,fruit yogurt,149,1.7,1.1,0.5,0.024,28,28,6,0,7.5,0.013,113,0.006,0.081,0.032,0.087,0.2,0.1,0.7,0.099,0.9,0,0.079,0.031,207,0.1,0.066,19.5,0.029,163.5,265.5,0.026,1,243.671 27 | 25,25,danone low fat alsafi,88,1.7,1,11.1,7.1,0.089,255,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.743 28 | 26,26,fruit n yogurt parfait mcdonalds,156,1.9,0.044,0.2,0.1,30.9,19.1,4.1,1.5,7.5,0.065,111.2,0,0.004,0.029,0.099,0.2,0.4,0.6,0,20.7,0,0,0,128.1,0.092,0.7,20.9,0.3,125.2,248.8,0,0.5,187.9 29 | 27,27,triple zero greek yogurt dannon oikos,120,0,0,15,7,22.5,6,0.038,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,22.598 30 | 28,28,greek yogurt,100,0.7,0.2,0.018,0.010,6.1,5.5,17.3,0,8.5,0.081,144.7,0.035,0.009,0.045,0.036,0.5,0.4,0.6,0.1,0,0,0.089,0.010,187,0.021,0.1,18.7,0.004,229.5,239.7,0.032,0.9,211.251 31 | 29,29,chocolate rasberry protein mini proteinfx,102,2.9,1.8,9.1,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.9 32 | 30,30,gushers fruit gushers,360,6,80,40,0,0.2,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,6.2 33 | 31,31,egg fried,90,6.8,2,2.8,1.5,0.4,0.2,6.3,0,184.5,0.043,32,0.016,0.049,0.035,0.029,0.2,0.093,0.8,0.093,0,0.081,0.6,0.079,28.5,0.065,0.9,6,0.046,98.9,69.9,0.078,0.6,42.904 34 | 32,32,glucerna abbott,1010,35.6,2.8,0,0,120.9,49.8,0,12.8,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,169.3 35 | 33,33,sports drink,26,0,0,0,0,7.2,0,0,0,0,0.073,232.3,0,0,0,0,0,0,0,0,15.1,0,0,0,0,0.098,0.1,2.4,0,21.6,24,0.088,0.056,22.4 36 | 34,34,peanut butter cookie protein syntha 6,200,6,15,2,22,5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,11.0 37 | 35,35,hanfprotein pulver rohkostqualitat veganz,107,3.1,0.3,2.3,1.6,14.2,0.057,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,17.3 38 | 36,36,smoothie high protein creamy chocolate slimfast,110,3.5,0.5,0,0,2.1,1,12,0,5,0.2,15,45,25,100,25,35,25,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,62.6 39 | 37,37,egg large,138,9.3,0.5,11.8,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,9.3 40 | 38,38,boysenberries frozen,66,0.3,0.049,0.059,0.2,16.1,9.1,1.5,7,0,0.064,113.4,0,0.063,0.070,0,0.045,1,0.3,0.054,4.1,0,1.1,0.002,35.6,0.1,1.1,21.1,0.7,35.6,183.5,0.058,0.3,65.7 41 | 39,39,malt beverage,88,0.3,0.005,0.032,0.1,19.1,19.1,0.5,0,0,0.020,216,0,0.022,0.021,0,0.1,2.6,0.020,0.044,1.2,0,0,0,16.6,0.048,0.1,16.6,0.046,37.9,19,0.049,0.025,37.8 42 | 40,40,chocolate powder for milk,88,0.5,0.5,0,0,19.9,18,1,1,0,0.012,0.2,0,0.041,0.079,0,0.090,0.064,0,0.2,6,0,0.038,0.041,100.1,0.2,0,11.7,0.2,18.9,61.6,0.039,1.5,128.5 43 | 41,41,vegetable fruit juice,9,0.017,0,0,0,2.2,0.6,0.071,0,0,0.043,27.2,0,0.047,0,0,0.075,0.023,0.025,0.050,9.6,0,0.5,0.036,0.9,0.031,0.013,0.3,0.032,0.6,5.6,0.027,0.069,12.912 44 | 42,42,cocoa powder unsweetened,12,0.7,0.4,0.2,0.048,3.1,0.087,1.1,1.8,0,0.062,0.2,0,0.049,0.078,0,0.058,0.1,0.054,0.028,0,0,0.085,0.056,6.9,0.2,0.7,26.9,0.2,39.6,82.3,0.035,0.4,14.3 45 | 43,43,coconut milk,35,3.6,3.2,0.2,0.010,0.8,0.5,0.3,0.3,0,0.001,10.1,0,0.058,0.094,0,0,0.1,0.045,0.069,0.4,0,0.063,0.014,2.4,0.023,0.2,5.6,0.1,15,39.5,0.096,0.1,8.0 46 | 44,44,cocoa powder,28,0.3,0.2,0.061,0.055,5.9,4.6,0.5,0.3,0,0.034,0.1,0.066,0.045,0.096,0.051,0.033,0.048,0.066,0.085,0.016,0,0.094,0.001,9.3,0.049,0.075,5.8,0.010,22.1,49.8,0.069,0.1,16.393 47 | 45,45,rice drink,113,2.3,0,1.5,0.8,22,12.7,0.7,0.7,0,0.058,214.3,0,0.2,0.010,0.000,0.008,0.3,0.9,0.4,0.059,0,0.042,1.1,0.078,283.2,0.013,0.5,26.4,0.7,134.4,64.8,0.009,25.883 48 | 46,46,coconut water,46,0.5,0.4,0.091,0.030,8.9,6.3,1.7,2.6,0,0.3,228,0,0.002,0.050,0,0.1,0.2,0.1,0.082,5.8,0,0,0,57.6,0.029,0.7,60,0.3,48,600,0.045,0.2,77.8 49 | 47,47,caramel med iced coffee dunkin donuts,268,9,41,37,5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,9.0 50 | 48,48,instant coffee with water decaffeinated,4,0,0.024,0,0.068,0.8,0,0.2,0,0,0.093,177.1,0,0,0,0,0.058,0.5,0.092,0,0,0,0,0,7.2,0.046,0.042,7.2,0.098,5.4,64.4,0.020,0.053,8.247 51 | 49,49,instant coffee powder,353,0.5,0.2,0.090,0.2,75.4,0,12.2,0,0,0.051,3.1,0,0.090,0,0,0.031,28.2,0.019,0.073,0,0,0,0.077,141,0.1,4.4,327,1.7,303,3535.00,0.024,0.4,233.5 52 | 50,50,instant coffee powder decaffeinated,6,0.069,0.015,0.031,0.075,1.4,0,0.2,0,0,0.090,0.051,0,0,0,0,0.072,0.5,0.077,0,0,0,0,0.015,2.5,0.031,0.056,5.6,0.084,5.1,63,0.015,0.004,4.121 53 | 51,51,instant cappuccino powder,53,0.7,0.4,0.2,0.098,11.2,9,0.4,0.2,0.3,0.074,0.2,0,0.073,0.067,0.037,0.087,0.3,0.037,0.011,0.100,0,0.080,0.062,17.3,0.078,0.036,5.6,0.014,23.8,108,0.064,0.058,29.908 54 | 52,52,coffee decaffeinated,0,0,0.087,0,0.099,0,0,0.2,0,0,0.058,235.3,0,0,0,0,0,0.5,0.091,0,0,0,0,0.023,4.7,0.028,0.1,11.9,0.083,2.4,128,0.042,0.084,5.0 55 | 53,53,espresso decaffeinated,3,0.011,0.037,0,0.057,0.5,0,0.065,0,0,0.011,29.3,0,0.087,0.054,0,0.003,1.6,0.086,0.035,0.026,0,0,0.047,0.6,0.085,0.087,24,0.093,2.1,34.5,0,0.036,1.391 56 | 54,54,instant coffee with water,4,0,0.051,0,0.025,0.6,0,0.2,0,0,0.015,177.4,0,0,0,0,0.073,0.4,0.089,0,0,0,0,0,7.2,0.026,0.015,7.2,0.001,5.4,53.7,0.054,0.013,8.027 57 | 55,55,coffee,2,0.069,0.011,0.016,0.011,0,0,0.3,0,0,0.075,235.6,0,0.034,0.076,0,0.2,0.5,0.6,0.022,0,0,0.068,0.025,4.7,0.045,0.051,7.1,0.076,7.1,116.1,0,0.039,5.09 58 | 56,56,espresso,3,0.086,0.068,0,0.054,0.5,0,0.021,0,0,0.083,29.3,0,0.088,0.073,0,0.080,1.6,0.075,0.099,0.099,0,0.005,0.027,0.6,0.022,0.049,24,0.023,2.1,34.5,0,0.020,1.263 59 | 57,57,whey protein powder vanilla eas,108,1.7,1.4,1.3,20.2,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.7 60 | 58,58,energy drink sugar free red bull,13,0.2,0,0,0,1.8,0,0.6,0,0,0.008,245.9,0,0.031,0,0.090,1.4,21.3,4.8,5,0,0,0,0,32.5,0.048,0.023,7.5,0.016,0,7.5,0.067,0,35.19 61 | 59,59,powerade coca cola,9,0.009,0.076,0.078,0.054,2.3,1.8,0,0,0,0.005,27.1,0,0.021,0,0.082,0,0.5,0,0.013,0,0,0,0,0.3,0,0.096,0,0,0.3,5.3,0,0.058,2.671 62 | 60,60,amp energy pepsi,110,0.2,0,0,0,29,29,0.6,0,0,0.093,209.5,0,0.078,0,0.083,0.8,4.8,2.4,0.5,0,0,0,0,31.2,0.044,0.053,7.2,0,40.8,7.2,0.075,0,61.067 63 | 61,61,gatorade g2 pepsi,2,0.052,0,0,0,0.6,0.4,0.069,0,0,0.011,28.9,0,0,0,0.091,0,0,0,0,0,0,0,0.038,0,0,0,0,0,2.1,3.8,0,0,0.69 64 | 62,62,amp energy sugar free pepsi,5,0,0,0,0,2.5,0,0,0,0,0.093,236,0,0,0,0.084,0.3,2,1,0.2,0,0,0,0,0,0.095,0,7.2,0,40.8,4.8,0.038,0,2.5 65 | 63,63,energy drink red bull,111,0,0,0,0,26.4,26.4,1.2,0,0,0.1,230.2,0,0,0.1,0,0.068,0.3,25.4,4.9,5.6,0,0,0,0,15.5,0.049,0.2,49,0.086,0,7.7,0.072,33.224 66 | 64,64,powerade zero ion4 coca cola,0,0,0,0,0,0,0,0,0,0,0.030,29.5,0,0.098,0,0.028,0,0.2,0,0.064,0.7,0,0.2,0.057,0.6,0.051,0.072,0.3,0,0,3,0,0.026,1.393 67 | 65,65,full throttle coca cola,110,0.2,0,0,0,29,29,0.6,0,0,0.071,209.5,0,0.037,0,0.031,0,4,0,0.4,0,0,0,0,31.2,0.054,0.037,7.2,0,0,7.2,0.083,0,61.049 68 | 66,66,passion fruit juice purple,148,0.4,0.064,0.037,0.3,35.7,35.2,1.7,0.5,0,0.084,208,0,0,0.039,0,0.2,5.5,0,0.1,45,0,0.090,0.011,9.9,0.1,0.9,42,0,61.8,686.7,0.001,0.1,94.1 69 | 67,67,pomegranate juice,134,0.7,0.2,0.1,0.1,32.7,31.5,0.4,0.2,0,0.089,214,0,0.037,0.014,0,0.006,0.6,0.7,0.019,0.2,0,0.9,0.081,27.4,0.060,0.2,17.4,0.2,27.4,532.9,0.065,0.2,61.8 70 | 68,68,cranberry apricot juice,157,0,0,0,0,39.7,0,0.5,0.2,0,0.007,204.6,0,0.089,0.071,0,0.010,0.3,0.071,0.082,0,0,0,0,22.1,0.051,0.4,7.4,0.3,12.3,149.5,0,0.050,62.9 71 | 69,69,pineapple juice,133,0.3,0.071,0.027,0.1,32.2,25,0.9,0.5,0,0.075,215.9,0,0,0.1,0.004,0,0.064,0.5,0.1,0.3,109.5,0,0.048,0.062,32.5,0.2,0.8,30,1.3,20,325,0.099,34.477 72 | 70,70,passion fruit juice yellow,126,0.1,0.008,0.020,0.094,33.6,33.1,1,0.5,0,0.003,211.5,0,0,0.100,0,0.3,3.6,0,0.1,73.6,0,0.036,0.090,9.9,0.1,0.6,42,0,32.1,686.7,0.098,0.1,119.3 73 | 71,71,orange apricot juice,128,0.3,0.001,0.078,0.063,31.8,30.2,0.8,0.3,0,0.096,216.8,0,0.022,0.094,0,0.021,0.5,0.2,0.080,50,0,0.3,0.054,12.5,0.024,0.3,12.5,0.091,20,200,0.086,0.1,96.0 74 | 72,72,tangerine juice,106,0.5,0.082,0.015,0.054,24.9,24.5,1.2,0.5,0,0.022,219.6,0,0,0.1,0.086,0,0.094,0.2,0.3,0.1,76.6,0,0.3,0,44.5,0.060,0.5,19.8,0.031,34.6,439.7,0.016,27.29 75 | 73,73,orange juice,90,0.4,0.079,0.046,0.037,20.8,16.8,1.4,0.4,0,0.067,176.6,0,0,0.2,0.002,0,0.010,0.8,0.4,0.052,100,0,0.013,0.053,22,0.060,0.4,22,0.056,34,400,0.067,23.177 76 | 74,74,grapefruit juice,96,0.2,0.011,0.090,0.008,22.7,0,1.2,0,0,0.061,222.3,0,0,0.060,0.038,0,0.010,0.5,0.5,0.1,93.9,0,0,0,22.2,0.004,0.5,29.6,0.037,37.1,400.1,0,24.254 77 | 75,75,pineapple grapefruit juice,118,0.3,0.077,0.000,0.022,29,28.8,0.5,0.3,0,0.054,219.8,0,0.042,0.047,0,0.070,0.7,0.1,0.1,115,0,0.001,0.046,17.5,0.1,0.8,15,1,15,152.5,0.025,0.2,163.4 78 | 76,76,tamarind nectar,143,0.3,0,0,0,37,31.9,0.2,1.3,0,0.046,213.3,0,0.077,0.055,0,0.024,0.2,0.025,0.048,17.8,0,0.3,0.069,25.1,0.054,1.9,10,0.079,5,67.8,0,0.089,83.6 79 | 77,77,apple juice,114,0.3,0.064,0.078,0.090,28,23.9,0.2,0.5,0,0.038,218.8,0,0,0.083,0,0,0.019,0.2,0.1,0.025,2.2,0,0.057,0,19.8,0.065,0.3,12.4,0.2,17.4,250.5,0.039,29.087 80 | 78,78,limeade,128,0,0,0,0,34.1,32.8,0,0,0,0.071,212.6,0,0.047,0.058,0,0.034,0.050,0.006,0.051,7.7,0,0,0,4.9,0.058,0,4.9,0.068,2.5,24.7,0.081,0.024,46.7 81 | 79,79,guava nectar,158,0.2,0.050,0.071,0.054,40.8,32.5,0.2,2.5,0,0.063,209.6,0,0,0.060,0.075,0,0.073,0.4,0.2,0.057,53,0,0.1,0.087,20.1,0.055,1,5,0.1,5,97.9,0,43.83 82 | 80,80,orange grapefruit juice,106,0.2,0.009,0.001,0.098,25.4,25.1,1.5,0.2,0,0.082,218.9,0,0.1,0.005,0,0.040,0.8,0.3,0.079,71.9,0,0.3,0.066,19.8,0.2,1.1,24.7,0.067,34.6,390.3,0.005,0.2,120.1 83 | 81,81,orange juice sainsburys,14,0.088,2.5,0.1,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.049 84 | 82,82,cranberry apple juice,154,0.3,0.066,0.014,0.1,38.8,35.5,0,0,0,0.061,205.3,0,0,0,0,0,0,0,0,96.8,0,0.3,0.075,7.4,0.070,0.2,2.5,0.100,4.9,41.7,0,0.015,143.5 85 | 83,83,pineapple orange juice,125,0,0,0,0,29.5,29,3.3,0.3,0,0.069,217.3,0,0.035,0.046,0,0.020,0.5,0.1,0.1,56.3,0,0.079,0.085,12.5,0.1,0.7,15,0.9,10,115,0.071,0.2,102.6 86 | 84,84,pear nectar,150,0.062,0.032,0.086,0.038,39.4,0,0.3,1.5,0,0.056,210,0,0,0.032,0.049,0,0.033,0.3,0.087,0.017,67.5,0,0,0,12.5,0.2,0.7,7.5,0.041,7.5,32.5,0.065,41.466 87 | 85,85,blackberry juice,95,1.5,0.095,0.1,0.9,19.5,19.3,0.8,0.3,0,0.072,227.3,0,0.076,0.091,0,0.053,1.1,0,0.075,28.3,0,2.3,0.088,30,0.3,1.2,52.5,0,30,337.5,0.093,1,81.6 88 | 86,86,lemon juice,54,0.6,0.060,0.073,0.038,16.6,6.1,0.9,0.7,0,0.046,225.2,0,0,0.015,0.093,0,0.084,0.2,0.2,0.1,94.4,0,0.4,0,14.6,0.073,0.2,14.6,0.091,19.5,251.3,0.090,18.9 89 | 87,87,cranberry grape juice,137,0.2,0.053,0.092,0.080,34.3,0,0.5,0.2,0,0.079,209.7,0,0.094,0.090,0,0.056,0.3,0.1,0.027,78.4,0,0,0,19.6,0.051,0.040,7.4,0.4,9.8,58.8,0,0.055,133.2 90 | 88,88,citrus fruit juice,114,0.057,0.088,0.048,0.085,28.3,20.2,0.8,0.2,0,0.051,217.8,0,0.043,0.021,0,0.096,0.2,0.2,0.054,67.2,0,0.1,0.042,22.3,0.065,2.8,14.9,0.2,24.8,277.8,0.036,0.011,121.7 91 | 89,89,prune juice,182,0.006,0.033,0.082,0.011,44.7,42.1,1.6,2.6,0,0.100,208,0,0.038,0,0,0.2,2,0.3,0.6,10.5,0,0.3,0.007,30.7,0.2,3,35.8,0.4,64,706.6,0.083,0.5,93.105 92 | 90,90,guanabana nectar,148,0.4,0.096,0.1,0.006,37.5,32.8,0.3,0.3,0,0.061,212.6,0,0.086,0.050,0,0.027,0.3,0.2,0.050,27.9,0,0.036,0,17.6,0.067,0.9,7.5,0.056,5,62.8,0.067,0.011,84.9 93 | 91,91,fruit juice,108,0.1,0.059,0.048,0.036,26.3,24.2,0.3,0.2,0,0.1,172.7,0,0.071,0.2,0,0,0.096,0.028,0,0.001,50.8,0,0.1,0,6,0.039,0.5,10,0,14,244,0.089,26.937 94 | 92,92,horchata,15,0.2,0,0,0,3.3,2.6,0.1,0,0,0.013,24.6,0,0,0,0,0,0.016,0,0.029,0,0,0.022,0,5.1,0.028,0.057,0.8,0.045,4.2,9.6,0.005,0.058,8.717 95 | 93,93,fruit punch,98,0.4,0.048,0.011,0.039,24.4,0,0.2,0,0,0.080,208.6,0,0.092,0.061,0,0.1,0.1,0.035,0.047,11.2,0,0,0,16.4,0.097,0.5,7,0.1,7,154.4,0,0.4,53.1 96 | 94,94,apricot nectar,139,0.2,0.070,0.085,0.039,35.7,34.2,0.9,1.5,0,0.025,210.5,0,0.044,0.059,0,0.014,0.6,0.2,0.036,1.5,0,0.8,0.008,17.4,0.2,0.9,12.4,0.008,22.3,282.7,0,0.2,58.1 97 | 95,95,orangensaft ja,86,0,0,17.8,17.8,1.4,0.024,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.4 98 | 96,96,fruchtcocktail granini,102,0.4,0.2,23,23,1,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.4 99 | 97,97,peach nectar,125,1.3,0.6,0.2,0.5,29.7,27.8,0.5,0.2,0,0.077,217.3,0,0.038,0.1,0.005,0,0.049,0.5,0.2,0.091,75.2,0,1.5,0,14.9,0.2,0.2,12.5,0.058,12.5,124.5,0.071,31.949 100 | 98,98,orange pineapple juice,15,0.080,0.056,0.026,0.024,3.6,3.1,0.1,0.032,0,0.044,25.7,0,0.044,0.010,0,0.069,0.054,0,0.053,12,0,0.036,0.016,2.4,0.090,0.086,3,0.1,3.2,44.3,0.076,0.072,18.261 101 | 99,99,cranberry juice,92,0.3,0.061,0.098,0.1,24.4,24.2,0.8,0.2,0,0.093,174.3,0,0,0.069,0.011,0,0.016,0.2,0,0.1,18.6,0,2.4,0.099,16,0.1,0.5,12,0,26,154,0.036,25.975 102 | 100,100,white grapefruit juice,96,0.2,0.083,0.091,0.007,22.7,22.5,1.2,0.2,0,0.018,222.3,0,0.015,0.029,0,0.005,0.5,0.5,0.1,93.9,0,0.5,0,22.2,0.035,0.5,29.6,0.032,37.1,400.1,0.019,0.1,140.9 103 | 101,101,mango nectar,128,0.2,0.040,0.092,0.066,32.9,31.2,0.3,0.8,0,0.061,217.4,0,0,0.039,0.055,0,0.060,0.2,0.2,0.038,38.2,0,0.5,0.047,42.7,0.087,0.9,7.5,0.007,5,60.2,0.025,34.447 104 | 102,102,acerola cherry juice,56,0.7,0.2,0.2,0.2,11.6,10.9,1,0.7,0,0.082,228.2,0,0.067,0.084,0,0.1,1,0.5,0.051,3872.00,0,0.4,0.057,24.2,0.2,1.2,29,0,21.8,234.7,0.061,0.2,3911.4 105 | 103,103,apple juice concentrate,350,0.8,0.1,0.053,0.2,86.5,81.9,1.1,0.8,0,0.049,120.3,0,0.029,0.005,0,0.1,0.3,0.5,0.2,4.4,0,0.022,0,42.2,0.1,1.9,35.9,0.5,52.8,945.3,0.067,0.3,137.7 106 | 104,104,papaya nectar,143,0.4,0.1,0.1,0.070,36.3,34.8,0.4,1.5,0,0.082,212.6,0,0,0.053,0.039,0,0.077,0.4,0.1,0.017,7.5,0,0.6,0.076,25,0.029,0.9,7.5,0.012,0,77.5,0.072,38.657 107 | 105,105,apple juice martinellis,122,0,0,29.1,0.7,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 108 | 106,106,lime juice,61,0.2,0.052,0.087,0.071,20.4,4.1,1,1,0,0.014,219.7,0,0.015,0.010,0,0.078,0.3,0.3,0.040,72.6,0,0.5,0.074,33.9,0.027,0.2,19.4,0.011,33.9,283.1,0.052,0.2,129.3 109 | 107,107,grape juice,152,0.3,0.060,0.030,0.059,37.4,35.9,0.9,0.5,0,0.003,213.8,0,0.050,0,0,0.025,0.3,0.1,0.086,0.3,0,0,0.025,27.8,0.025,0.6,25.3,0.6,35.4,263.1,0,0.2,67.8 110 | 108,108,water dannon,0,0,0,0,0,0,0,0,0,0,0,330.9,0,0,0,0,0,0,0,0,0,0,0,0,9.9,0,0,3.3,0,0,0,0,0,9.9 111 | 109,109,tap water,0,0,0,0,0,0,0,0,0,0,0.018,236.8,0,0,0,0,0,0,0,0,0,0,0,0,7.1,0.086,0,2.4,0,0,0,0,0.042,7.1 112 | 110,110,table water,0,0,0,0,0,0,0,0,0,0,0.044,499.5,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,70.0 113 | 111,111,fruit flavored water,52,0,0,0,0,13,13,0,0,0,0,223.8,0.050,0,0.016,0.049,0,2,1,0.2,30.1,0,4.5,0,40.3,0.061,0,7.1,0,0,0,0,0.8,83.471 114 | 112,112,water,0,0,0,0,0,0,0,0,0,0,0.024,237,0,0,0,0,0,0,0,0,0,0,0,0,23.7,0.098,0,4.7,0,0,0,0,0,23.7 115 | 113,113,cola light coca cola,0,0,0,0,0,0,0,0,0,0,0.087,336.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 116 | 114,114,lemon lime soda,151,0,0,0,0,38.4,37.6,0.3,0,0,0.024,330.2,0,0,0,0,0,0.055,0,0,0,0,0,0,7.4,0,0.061,3.7,0,0,3.7,0,0.065,46.113 117 | 115,115,tonic water,114,0,0,0,0,29.6,29.6,0,0,0,0.049,306.1,0,0,0,0,0,0,0,0,0,0,0,0,3.4,0.031,0.090,0,0.050,0,0,0,0.3,33.038 118 | 116,116,lemonade low calorie,7,0,0,0,0.000,1.6,0,0.092,0,0,0.013,236.8,0,0,0,0,0,0,0,0,7.6,0,0,0,66.9,0.040,0.1,2.4,0.057,31.1,2.4,0,0.011,76.216 119 | 117,117,ginger ale,68,0,0,0,0,17.5,17.4,0,0,0,0.043,182.5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0.024,0.4,2,0.087,0,2,0.078,17.528 120 | 118,118,ice tea lemon flavor nestle,88,0,0,0,0,22.3,22.3,0,0,0,0.063,223.4,0,0,0,0,0,0,0,0,0,0,0,0,7.4,0.068,0,2.5,0.3,88.2,46.6,0,0.1,29.7 121 | 119,119,sprite coca cola,80,0.063,0,0,0,20.3,18,0.1,0,0,0.021,179.6,0,0,0,0,0,0,0.032,0,0,0,0,0,0,4,0.042,0.2,2,0.017,0,2,0,20.538 122 | 120,120,cola coca cola,151,0,0,0,0,38.9,38.9,0,0,0,0.045,329.8,0,0,0,0,0,0,0,0,0,0,0,0,7.4,0,0.048,0,0,40.5,11,0.045,0.052,46.4 123 | 121,121,cola,74,0.070,0,0,0,19.1,17.9,0.1,0,0,0.072,180.6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0.012,0.2,0,0.081,20,4,0.038,19.336 124 | 122,122,cream soda,252,0,0,0,0,65.7,65.7,0,0,0,0.098,428.3,0,0,0,0,0,0,0,0,0,0,0,0,24.7,0.036,0.2,4.9,0.080,0,4.9,0,0.3,90.6 125 | 123,123,club soda,0,0,0,0,0,0,0,0,0,0,0.046,199.8,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0.015,0.078,2,0.052,0,4,0,0.025 126 | 124,124,pink lemonade,86,0.3,0.079,0.030,0.034,21.6,20.6,0.1,0.2,0,0.010,177.8,0,0.003,0.078,0,0,0.047,0.035,0.061,6.2,0,0.062,0,8,0.011,0.092,4,0.094,4,34,0,0.093,36.488 127 | 125,125,grape soda,160,0,0,0,0,41.7,0,0,0,0,0.042,330.3,0,0,0,0,0,0,0,0,0,0,0,0,11.2,0.038,0.3,3.7,0.054,0,3.7,0,0.3,53.2 128 | 126,126,cola without caffeine coca cola,151,0,0,0,0,38.9,38.9,0,0,0,0.040,329.8,0,0,0,0,0,0,0,0,0,0,0,0,7.4,0,0.026,0,0,40.5,11,0.063,0.032,46.381 129 | 127,127,white lemonade,80,0.003,0.093,0.024,0.076,20.8,20,0.1,0,0,0.020,178.7,0,0,0.004,0.043,0,0.020,0.094,0.094,0.007,7.8,0,0.040,0,8,0.013,0.3,4,0.083,4,30,0.066,21.02 130 | 128,128,chocolate soda,207,0,0,0,0,52.6,52.6,0,0,0,0.031,438.9,0,0,0,0,0,0,0,0,0,0,0,0,19.7,0.022,0.5,4.9,0.2,4.9,246,0.015,0.8,72.8 131 | 129,129,sweet tea lipton,130,0,0,0,0,35,35,0,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,35.0 132 | 130,130,hibiscus tea,74,1.3,0.5,0.2,0.040,14.8,12,0.9,0.6,0,0.091,179.3,0,2.6,0.030,0,0.2,0,0,0,36.8,0,0,0,2,0.1,17.3,2,0,6,18,0,0.2,73.7 133 | 131,131,instant lemon tea,2,0,0,0,0,0.3,0.047,0.1,0,0,0.046,199.2,0,0,0,0,0,0.003,0.023,0.060,0.009,0,0,0,0,6,0.026,0.020,4,0.8,2,36,0,0.494 134 | 132,132,pesca tea santal,116,0,28.1,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 135 | 133,133,instant lemon tea sweetened,91,0.2,0.018,0.046,0.093,22.3,21.6,0.071,0.3,0,0.089,236.2,0,0.083,0,0,0,0.006,0,0.044,0,0,0,0,5.2,0.076,0.000,2.6,0.3,0,38.9,0.017,0.048,28.147 136 | 134,134,tea with milk and sugar yorkshire,15,0.5,0.3,0.7,0.7,0.4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9 137 | 135,135,tea blueberry muffin tekanne,4,0.2,0.2,0.6,0.4,0.4,0.007,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.6 138 | 136,136,chamomile tea,2,0,0.028,0.032,0.073,0.4,0,0,0,0,0.003,199.4,0,0.060,0.050,0,0.022,0,0.027,0,0,0,0,0,4,0.097,0.2,2,0.078,0,18,0,0.006,4.6 139 | 137,137,black tea,2,0,0.072,0.032,0.029,0.7,0,0,0,0,0.091,236.3,0,0,0.041,0,0.018,0,0.040,0,0,0,0,0,0,0.088,0.088,7.1,0.5,2.4,87.7,0,0.081,0.72 140 | 138,138,herb tea,2,0,0.014,0.043,0.071,0.4,0,0,0,0,0.064,199.4,0,0.007,0.093,0,0.051,0,0.096,0,0,0,0,0,4,0.059,0.2,2,0.019,0,18,0,0.006,4.6 141 | 139,139,clam tomato juice,80,0.3,0,0,0,18.2,5.5,1,0.7,0,0.6,145.1,0,0.015,0.079,0,0.064,0.4,0.1,0.1,8.3,0,0.2,0,13.3,0.011,0.2,8.3,0.073,18.3,147.7,0.097,0.1,42.0 142 | 140,140,tomato juice,41,0.7,0.043,0.062,0.043,8.6,6.3,2.1,1,0,0.063,229,0,0.069,0.2,0.003,0,0.2,1.6,0.6,0.2,170.3,0,0.8,0.056,24.3,0.1,0.9,26.7,0.2,46.2,527.3,0.067,12.742 143 | 141,141,vegetable juice,53,0.2,0.071,0.016,0.1,11.1,7.9,1.5,1.9,0,0.1,226.3,0,0,0.095,0.039,0,0.091,1.8,0.6,0.3,67,0,0.8,0.007,26.6,0.5,1,26.6,0.2,41.1,467.1,0.026,15.538 144 | 142,142,tomato vegetable juice,53,0.2,0.014,0.014,0.1,11.1,9.2,1.5,1.9,0,0.2,226.3,0,0.034,0.047,0,0.097,1.8,0,0.3,67,0,0.8,0.085,26.6,0.5,1,26.6,0,41.1,467.1,0.046,0.5,109.3 145 | 143,143,carrot juice,80,0.3,0.055,0.027,0.1,18.6,7.8,1.9,1.6,0,0.1,177.7,0,0.2,0.077,0,0.1,0.8,0.5,0.4,17,0,2.3,0.047,48,0.087,0.9,28,0.3,84,584,0.046,0.4,88.3 146 | 144,144,acorn dried,144,8.9,1.2,5.6,1.7,15.2,0,2.3,0,0,0,1.4,0,0.005,0.085,0,0.021,0.7,0.3,0.2,0,0,0,0,15.3,0.2,0.3,23.2,0.4,29.1,200.6,0,0.2,42.0 147 | 145,145,fenugreek seeds,36,0.7,0.2,0,0,6.5,0,2.6,2.7,0,0.090,1,0,0.036,0.046,0,0.019,0.2,0,0.064,0.3,0,0,0,19.5,0.1,3.7,21.2,0.1,32.9,85.5,0.025,0.3,36.0 148 | 146,146,peanut topping mcdonalds,181,15,2.4,7.3,4,4.6,1.2,7.9,0,0,0,0.2,0,0.070,0.015,0,0.099,4.7,0.3,0.2,0,0,2.3,0,13,0.1,0.4,54.6,0.6,103,179.4,0,0.8,40.9 149 | 147,147,beechnuts dried,173,15,1.7,6.6,6,10.1,0,1.9,0,0,0.068,2,0,0.053,0.087,0,0.1,0.3,0.3,0.2,4.7,0,0,0,0.3,0.2,0.7,0,0.4,0,305.1,0,0.1,32.7 150 | 148,148,breadnut tree seeds raw,61,0.3,0.089,0.061,0.1,13.1,0,1.7,0,0,0.084,12.7,0,0.040,0.077,0,0.053,0.2,0.3,0.1,7.8,0,0,0,27.7,0.4,0.6,19.2,0.015,19,334.8,0,0.3,51.2 151 | 149,149,cumin seeds,8,0.5,0.096,0.3,0.013,0.9,0.063,0.4,0.2,0,0.073,0.2,0,0.079,0.015,0,0.093,0.064,0,0.021,0.2,0,0.096,0.001,19.6,0.058,1.4,7.7,0.087,10.5,37.5,0.072,0.1,23.2 152 | 150,150,pili nuts dried,863,95.5,37.4,44.7,9.1,4.8,0,13,0,0,0.014,3.3,0,1.1,0.066,0,0.1,0.6,0.6,0.1,0.7,0,0,0,174,1.1,4.2,362.4,2.8,690,608.4,0,3.6,292.2 153 | 151,151,hazelnuts roasted,194,18.7,1.4,14,2.5,5.3,1.5,4.5,2.8,0,0,0.8,0,0.1,0.031,0,0.074,0.6,0.3,0.2,1.1,0,4.6,0,36.9,0.5,1.3,51.9,1.7,93,226.5,0.061,0.8,70.6 154 | 152,152,lupins cooked,193,4.8,0.6,2,1.2,15.4,0,25.8,4.6,0,0.4,118,0,0.2,0.083,0,0.071,0.8,0.3,0.016,1.8,0,0,0,84.7,0.4,2,89.6,1.1,212.5,406.7,0.084,2.3,139.1 155 | 153,153,hazelnuts blanched,178,17.3,1.3,13.6,1.6,4.8,1,3.9,3.1,0,0,1.6,0,0.1,0.021,0,0.049,0.4,0.2,0.2,0.6,0,5,0,42.2,0.5,0.9,45.3,3.6,87.7,186.2,0.015,0.6,72.8 156 | 154,154,brazilnuts dried,33,3.3,0.8,1.2,1,0.6,0.1,0.7,0.4,0,0.073,0.2,0,0.079,0.035,0,0.013,0.083,0.051,0.041,0.024,0,0.3,0,8,0.074,0.1,18.8,0.084,36.3,33,0.035,0.2,13.139 157 | 155,155,sunflower seeds dry roasted,47,4,0.4,0.8,2.6,1.9,0.2,1.5,0.9,0,0.067,0.076,0,0.087,0.072,0,0.086,0.6,0.6,0.010,0.1,0,2.1,0.037,5.6,0.1,0.3,10.3,0.2,92.4,68,0.021,0.4,14.3 158 | 156,156,ginkgo nuts canned,172,2.5,0.5,0.9,0.9,34.3,0,3.5,14.4,0,0.5,113.1,0,0.2,0.060,0,0.052,5.6,0.2,0.3,14.1,0,0,0,6.2,0.3,0.4,24.8,0.1,83.7,279,0.067,0.3,75.4 159 | 157,157,peanuts cooked,572,39.6,5.5,19.7,12.5,38.3,4.4,24.3,15.8,0,1.4,75.2,0,0.5,0.1,0,0.1,9.5,1.5,0.3,0,0,7.4,0,99,0.9,1.8,183.6,1.8,356.4,324,0.027,3.3,218.8 160 | 158,158,pumpkin squash seeds dried,721,63.3,11.2,21,27.1,13.8,1.8,39,7.7,0,0.013,6.7,0,0.4,0.024,0,0.2,6.4,1,0.2,2.5,0,2.8,0.030,59.3,1.7,11.4,763.7,5.9,1590.60,1043.60,0.078,10.1,197.0 161 | 159,159,cashew nuts raw,156,12.4,2.2,6.7,2.2,8.5,1.7,5.2,0.9,0,0.020,1.5,0,0.1,0.076,0,0.071,0.3,0.2,0.1,0.1,0,0.3,0.040,10.5,0.6,1.9,82.6,0.5,167.8,186.8,0.039,1.6,39.5 162 | 160,160,sunflower seeds dried,269,23.7,2,8.5,10.6,9.2,1.2,9.6,4,0,0.014,2.2,0,0.7,0.1,0,0.2,3.8,0.5,0.6,0.6,0,16.2,0,35.9,0.8,2.4,149.5,0.9,303.6,296.7,0.077,2.3,85.4 163 | 161,161,lotus seeds dried,106,0.6,0.1,0.1,0.4,20.6,0,4.9,0,0,0.024,4.5,0,0.2,0.019,0,0.059,0.5,0.3,0.2,0,0,0,0,52.2,0.1,1.1,67.2,0.7,200.3,437.8,0,0.3,79.4 164 | 162,162,butternuts dried,734,68.4,1.6,12.5,51.3,14.5,0,29.9,5.6,0,0.062,4,0,0.5,0.088,0,0.2,1.3,0.8,0.7,3.8,0,0,0,63.6,0.5,4.8,284.4,7.9,535.2,505.2,0.028,3.8,190.6 165 | 163,163,sesame seeds dried,52,4.5,0.6,1.7,2,2.1,0.031,1.6,1.1,0,0.049,0.4,0,0.037,0.050,0,0.057,0.4,0.082,0.096,0,0,0.099,0,87.8,0.4,1.3,31.6,0.2,56.6,42.1,0.047,0.7,98.4 166 | 164,164,breadfruit seeds,57,1.7,0.5,0.2,0.9,8.8,0,2.2,1.6,0,0.040,16.9,0,0.1,0.095,0,0.066,0.1,0.3,0.024,2,0,0,0,10.8,0.3,1.1,16.2,0.049,52.5,282.3,0,0.3,28.2 167 | 165,165,pinyon pine nuts dried,1,0.073,0.022,0.044,0.074,0.010,0,0.083,0.005,0,0.070,0.045,0,0.020,0.066,0,0.040,0.027,0.000,0.048,0.004,0,0,0,0.022,0.078,0.007,0.2,0.012,0.098,0.6,0,0.049,0.359 168 | 166,166,pine nuts dried,909,92.3,6.6,25.3,46,17.7,4.8,18.5,5,0,0.086,3.1,0,0.5,0.018,0,0.3,5.9,0.4,0.1,1.1,0,12.6,0.002,21.6,1.8,7.5,338.9,11.9,776.3,806,0.061,8.7,163.7 169 | 167,167,flaxseeds,55,4.3,0.4,0.8,3,3,0.2,1.9,2.8,0,0.035,0.7,0,0.2,0.015,0,0.095,0.3,0.1,0.056,0.023,0,0.042,0.053,26.3,0.1,0.6,40.4,0.3,66.1,83.7,0.050,0.4,38.994 170 | 168,168,safflower seed kernels dried,146,10.9,1,1.4,8,9.7,0,4.6,0,0,0.042,1.6,0,0.3,0.055,0,0.1,0.6,1.1,0.3,0,0,0,0,22.1,0.5,1.4,99.9,0.6,182.3,194.4,0,1.4,48.7 171 | 169,169,pistachio nuts raw,691,55.8,6.8,29.3,16.9,33.8,9.4,24.9,12.7,0,0.037,4.8,0,1.1,0.087,0,0.2,1.6,0.6,2.1,6.9,0,2.8,0,129.2,1.6,4.8,148.8,1.5,602.7,1260.80,0.078,2.7,268.1 172 | 170,170,peanuts raw,828,71.9,10,35.7,22.7,23.5,5.8,37.7,12.4,0,0.038,9.5,0,0.9,0.4,0,0.2,17.6,2.6,0.5,0,0,12.2,0,134.3,1.7,6.7,245.3,2.8,549,1029.30,0.032,4.8,286.5 173 | 171,171,poppy seeds,46,3.7,0.4,0.5,2.5,2.5,0.3,1.6,1.7,0,0.079,0.5,0,0.034,0.044,0,0.040,0.069,0.071,0.090,0.061,0,0.2,0,126.5,0.1,0.9,30.5,0.6,76.6,63.3,0.059,0.7,136.934 174 | 172,172,valencia peanuts raw,832,69.5,10.7,31.3,24.1,30.5,0,36.6,12.7,0,0.094,6.2,0,0.9,0.4,0,0.4,18.8,2.6,0.5,0,0,0,0,90.5,1.7,3.1,268.6,2.9,490.6,484.7,0.056,4.9,242.9 175 | 173,173,spanish peanuts,832,72.4,11.2,32.6,25.1,23.1,0,38.2,13.9,0,0.036,9.3,0,1,0.4,0,0.2,23.3,2.6,0.5,0,0,0,0,154.8,1.3,5.7,274.5,3.9,566.5,1086.20,0.077,3.1,308.1 176 | 174,174,dill seeds,20,1,0.018,0.6,0.041,3.6,0,1.1,1.4,0,0.037,0.5,0,0.011,0.085,0,0.059,0.2,0,0.001,1.4,0,0,0,100.1,0.009,1.1,16.9,0.1,18.3,78.3,0.075,0.3,109.7 177 | 175,175,caraway seeds,22,1,0.030,0.5,0.2,3.3,0.038,1.3,2.5,0,0.014,0.7,0,0.096,0.052,0,0.075,0.2,0,0.099,1.4,0,0.2,0,46.2,0.077,1.1,17.3,0.034,38.1,90.5,0.062,0.4,56.8 178 | 176,176,acorn raw,110,6.8,0.9,4.3,1.3,11.5,0,1.7,0,0,0,7.9,0,0.016,0.060,0,0.045,0.5,0.2,0.1,0,0,0,0,11.6,0.2,0.2,17.5,0.4,22.4,152.5,0,0.1,31.8 179 | 177,177,almonds roasted,182,16.6,1.3,10.4,4.1,5.3,1.4,6.4,3.2,0,0.029,0.8,0,0.055,0.032,0,0.2,1.1,0.049,0.065,0,0,7.8,0,87.3,0.3,1.1,82.2,0.7,139.8,209.7,0.017,0.9,119.9 180 | 178,178,lotus seeds raw,25,0.1,0.097,0.011,0.002,4.9,0,1.2,0,0,0.020,21.8,0,0.073,0.052,0,0.070,0.1,0.088,0.074,0,0,0,0,12.5,0.079,0.3,15.8,0.2,47.5,103.9,0,0.043,19.0 181 | 179,179,chestnuts cooked,37,0.4,0.016,0.1,0.2,7.9,0,0.6,0,0,0.046,19.3,0,0.095,0.099,0,0.053,0.2,0.046,0.021,7.6,0,0,0,13,0.1,0.5,15.3,0.2,28,202.3,0,0.020,30.0 182 | 180,180,alfalfa seeds,1,0.075,0.029,0.067,0.057,0.002,0.058,0.1,0.015,0,0.048,2.8,0,0.065,0.008,0,0.078,0.020,0.051,0.021,0.2,0,0.036,0.042,1,0.048,0.034,0.8,0.024,2.1,2.4,0.076,0.071,1.435 183 | 181,181,virginia peanuts roasted,173,14.6,1.9,7.6,4.4,6,0,7.8,2.7,0,0.018,0.7,0,0.066,0.061,0,0.022,4.4,0.4,0.099,0,0,0,0,25.8,0.4,0.5,56.4,0.6,151.8,195.6,0.006,2,57.4 184 | 182,182,hazelnuts raw,471,45.6,3.3,34.2,5.9,12.5,3.3,11.2,7.3,0,0,4,0,0.5,0.034,0,0.022,1.4,0.7,0.4,4.7,0,11.3,0.097,85.5,1.3,3.5,122.3,4.6,217.5,510,0.012,1.8,170.3 185 | 183,183,anise seeds,7,0.3,0.060,0.2,0.066,1.1,0,0.4,0.3,0,0.100,0.2,0,0.058,0.002,0,0.091,0.090,0.058,0.010,0.4,0,0,0,13.6,0.024,0.8,3.6,0.031,9.2,30.3,0.050,0.1,16.9 186 | 184,184,macadamia nuts roasted,215,22.8,3.6,17.8,0.4,4,1.2,2.3,2.4,0,0.064,0.5,0,0.2,0.002,0,0.086,0.7,0.2,0.1,0.2,0,0.2,0,21,0.2,0.8,35.4,0.9,59.4,108.9,0.085,0.4,53.5 187 | 185,185,studentenfutter alnatura,139,7.8,0.9,4.8,2,13.2,11.7,3.1,0.031,1.7,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,24.152 188 | 186,186,pumpkin squash seeds roasted,46,3.9,0.7,1.3,1.6,1.2,0.1,2.4,0.5,0,0.045,0.2,0,0.046,0.041,0,0.046,0.4,0.020,0.012,0.1,0,0.049,0.002,4.2,0.1,0.6,44,0.4,93.9,63,0.050,0.6,12.9 189 | 187,187,water chestnut raw,120,0.1,0.039,0.055,0.089,29.7,6,1.7,3.7,0,0.097,91.1,0,0.2,0.011,0,0.2,1.2,0.6,0.4,5,0,1.5,0.058,13.6,0.4,0.034,27.3,0.4,78.1,724.2,0.004,0.6,53.84 190 | 188,188,chestnuts roasted,37,0.3,0.009,0.1,0.1,7.9,1.6,0.5,0.8,0,0.043,6.1,0,0.080,0.096,0,0.011,0.2,0.046,0.075,3.9,0,0.080,0.008,4.4,0.075,0.1,5,0.2,16.1,88.8,0.037,0.086,17.9 191 | 189,189,ginkgo nuts raw,52,0.5,0.029,0.2,0.2,10.6,0,1.2,0,0,0.067,15.6,0,0.013,0.085,0,0.040,1.7,0.078,0.098,4.2,0,0,0,0.6,0.029,0.3,7.6,0.027,35.1,144.3,0,0.079,17.4 192 | 190,190,almonds raw,9,0.7,0.040,0.5,0.2,0.3,0.026,0.3,0.2,0,0.043,0.026,0,0.079,0.073,0,0.066,0.052,0.068,0.029,0,0,0.4,0,4,0.007,0.082,4,0.007,7.3,10.6,0.013,0.061,5.554 193 | 191,191,peanuts roasted,180,15.8,2.6,7.8,4.6,4.6,1.3,8.4,2.8,0,0.069,0.4,0,0.057,0.051,0,0.020,4.1,0.4,0.1,0.2,0,2.1,0,18.3,0.2,0.5,52.8,0.6,119.1,217.8,0.066,1,50.6 194 | 192,192,sunflower seeds toasted,829,76.1,8,14.5,50.3,27.6,0,23.1,15.4,0,0.8,1.3,0,0.4,0.3,0,0.4,5.6,9.5,1.1,1.9,0,0,0,76.4,2.5,9.1,172.9,2.8,1551.70,657.9,0.066,7.1,229.6 195 | 193,193,sisymbrium seeds,235,3.4,0.7,1.1,1.5,43.1,0,9,0,0,0.047,4.5,0,0.1,0.072,0,0.3,12.5,0.7,0.6,22.7,0,0,0,1208.40,0.052,0.098,232.4,1.1,4.4,1576.20,0,0.2,1286.643 196 | 194,194,breadnut tree seeds dried,587,2.7,0.7,0.3,1.4,127,0,13.8,23.8,0,0.091,10.4,0,0.040,0.2,0,0.2,3.4,3,1.1,74.6,0,0,0,150.4,3.9,7.4,184,0.5,284.8,3217.60,0.011,3.1,399.7 197 | 195,195,pecans raw,753,78.4,6.7,44.5,23.6,15.1,4.3,10,10.5,0,0,3.8,0,0.7,0.019,0,0.1,1.3,0.9,0.2,1.2,0,1.5,0.033,76.3,1.3,2.8,131.9,4.9,301.9,446.9,0.039,4.9,194.3 198 | 196,196,chia seeds dried,138,8.7,0.9,0.7,6.7,11.9,0,4.7,9.7,0,0.047,1.6,0,0.2,0.098,0,0.080,2.5,0,0,0.5,0,0.1,0,178.6,0.3,2.2,94.8,0.8,243.4,115.2,0.035,1.3,216.3 199 | 197,197,japanese chestnuts roasted,57,0.2,0.080,0.1,0.094,12.8,0,0.8,0,0,0.079,14.1,0,0.1,0.022,0,0,0.2,0.1,0.1,7.9,0,0,0,9.9,0.2,0.6,18.1,0.6,26.3,120.8,0,0.4,32.2 200 | 198,198,sesame seeds toasted,45,3.8,0.5,1.5,1.7,2.1,0,1.4,1.1,0,0.070,0.3,0,0.084,0.075,0,0.022,0.4,0.074,0.002,0,0,0,0,79.1,0.2,1.2,28.5,0.2,51,38,0.030,0.6,88.7 201 | 199,199,cashew nuts roasted,174,14.3,2.5,7.8,2.6,9,1.5,5.1,1,0,0.091,0.7,0,0.1,0.080,0,0.025,0.5,0.3,0.056,0.071,0,0.3,0.041,12.9,0.6,1.8,81.9,0.5,159.3,189.6,0.020,1.6,44.12 202 | 200,200,lupins raw,668,17.5,2.1,7.1,4.4,72.7,0,65.1,34,0,0.009,18.8,0,1.2,0.6,0,0.4,3.9,1.4,0.6,8.6,0,0,0,316.8,1.8,7.8,356.4,4.3,792,1823.40,0.090,8.6,522.5 203 | 201,201,mustard seeds ground,10,0.7,0.012,0.5,0.2,0.6,0.1,0.5,0.2,0,0.017,0.1,0,0.072,0.093,0,0.020,0.093,0.018,0.000,0.1,0,0.1,0.086,5.3,0.055,0.2,7.4,0.078,16.6,14.8,0.003,0.1,7.6 204 | 202,202,mixed nuts roasted,178,15.4,2,9.4,3.2,7.6,1.4,5.2,2.7,0,0.1,0.5,0,0.063,0.008,0,0.074,1.4,0.4,0.073,0.1,0,3.3,0.017,21,0.4,1.1,67.5,0.6,130.5,207.9,0.053,1.1,53.1 205 | 203,203,salat mix alestro,61,5.2,0.9,0.1,0.3,2.4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.6 206 | 204,204,chestnuts raw,309,3.3,0.6,1.1,1.3,66,0,3.5,11.7,0,0.020,70.5,0,0.3,0.030,0,0.2,1.7,0.7,0.5,62.4,0,0,0,39.2,0.6,1.5,46.4,1.4,134.9,751.1,0,0.8,187.6 207 | 205,205,pistachio nuts roasted,172,13.7,1.7,7.4,4,8.5,2.3,6.3,3.1,0,0.008,0.6,0,0,0.2,0.009,0,0.072,0.4,0.2,0.3,0.9,0,0.7,0.054,32.1,0.4,1.2,32.7,0.4,140.7,302.1,0.021,32.348 208 | 206,206,walnut,33,3.3,0.3,0.4,2.4,0.7,0.1,0.8,0.3,0,0.041,0.2,0,0.064,0.004,0,0.037,0.075,0.029,0.018,0.037,0,0.060,0.029,4.9,0.058,0.1,7.9,0.2,17.3,22.1,0.060,0.2,10.109 209 | 207,207,celery seeds,25,1.6,0.1,1,0.2,2.7,0.078,1.2,0.8,0,0.068,0.4,0,0.098,0.050,0,0.022,0.2,0,0.039,1.1,0,0.060,0,114.9,0.029,2.9,28.6,0.5,35.6,91,0.057,0.5,125.2 210 | 208,208,hickorynuts dried,788,77.2,8.4,39.1,26.3,21.9,0,15.3,7.7,0,0.053,3.2,0,1,0.042,0,0.2,1.1,2.1,0.2,2.4,0,0,0,73.2,0.9,2.5,207.6,5.5,403.2,523.2,0.038,5.2,200.2 211 | 209,209,spanish peanuts roasted,851,72.1,11.1,32.4,25,25.7,0,41.2,13.1,0,0.6,2.6,0,0.5,0.2,0,0.1,22,2,0.4,0,0,0,0,147,1,3.4,247,3.5,568.9,1140.70,0.083,2.9,302.5 212 | 210,210,woca seeds dried,102,0.3,0.025,0.044,0.059,22.6,0.1,2.2,5.4,0,0.048,2.6,0,0.029,0.092,0,0.042,1.2,0.4,0.3,0,0,10.1,0,7.6,0.041,0.4,24.3,0.3,64.8,156.5,0.021,1.8,38.5 213 | 211,211,pecans roasted,215,22.6,2.2,12.3,7.1,3.9,1.2,2.8,2.9,0,0.062,0.3,0,0.1,0.053,0,0.020,0.4,0.2,0.071,0.2,0,0.8,0,20.1,0.4,0.7,36.3,1.1,78.9,117.6,0.004,1.3,53.2 214 | 212,212,fennel seeds,7,0.3,0.008,0.2,0.065,1,0,0.3,0.8,0,0.097,0.2,0,0.021,0,0,0.092,0.1,0,0.045,0.4,0,0,0,23.9,0.051,0.4,7.7,0.1,9.7,33.9,0,0.042,27.1 215 | 213,213,chinese chestnuts roasted,68,0.3,0.027,0.2,0.073,14.8,0,1.3,0,0,0.052,11.4,0,0.036,0.100,0,0.037,0.4,0.2,0.1,10.9,0,0,0,5.4,0.1,0.4,25.5,0.5,28.9,135,0,0.3,33.1 216 | 214,214,watermelon seed kernels dried,602,51.2,10.6,8,30.3,16.5,0,30.6,0,0,0.1,5.5,0,0.2,0.038,0,0.2,3.8,0.4,0.032,0,0,0,0,58.3,0.7,7.9,556.2,1.7,815.4,699.8,0,11.1,164.5 217 | 215,215,macadamia nuts raw,38,4,0.6,3.1,0.059,0.7,0.2,0.4,0.5,0,0.011,0.011,0,0.032,0.061,0,0.042,0.1,0.066,0.035,0.062,0,0.058,0,4.5,0.009,0.2,6.8,0.2,9.9,19.3,0.064,0.034,10.387 218 | 216,216,pectin,163,0.2,0.052,0.093,0.076,45.2,0,0.2,4.3,0,0.1,4.4,0,0.042,0.025,0,0.021,0.017,0.023,0.063,0,0,0,0,3.5,0.2,1.4,0.5,0.094,1,3.5,0,0.2,54.8 219 | 217,217,black walnut dried,31,3,0.2,0.8,1.8,0.5,0.018,1.2,0.3,0,0.028,0.2,0,0.014,0.083,0,0.097,0.036,0.056,0.067,0.032,0,0.091,0.029,3.1,0.071,0.2,10.1,0.2,25.7,26.2,0.046,0.2,8.347 220 | 218,218,water chestnut canned,70,0.015,0.050,0.068,0.053,17.2,3.4,1.2,3.5,0,0.011,121,0,0.035,0.016,0,0.030,0.5,0.3,0.2,1.8,0,0.7,0.043,5.6,0.1,1.2,7,0.2,26.6,165.2,0.099,0.5,30.594 221 | 219,219,ginkgo nuts dried,98,0.6,0.1,0.2,0.2,20.5,0,2.9,0,0,0.045,3.5,0,0.1,0.054,0,0.010,3.3,0.4,0.2,8.3,0,0,0,5.7,0.2,0.5,15,0.059,76.1,282.4,0,0.2,38.5 222 | 220,220,virginia peanuts raw,822,71.2,9.3,36.9,21.5,24.1,5.8,36.8,12.4,0,0.008,10.1,0,1,0.3,0,0.2,18.1,2.6,0.5,0,0,9.6,0,129.9,1.6,3.7,249.7,2.5,554.8,1007.40,0.042,6.5,278.1 223 | 221,221,valencia peanuts roasted,177,15.4,2.4,6.9,5.3,4.9,0,8.1,2.7,0,0.004,0.6,0,0.052,0.003,0,0.068,4.3,0.4,0.072,0,0,0,0,16.2,0.3,0.5,48,0.5,95.7,183.6,0.046,0.9,47.8 224 | 222,222,nesfit diet cereal nestle,75,0,14.5,2.8,0,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 225 | 223,223,corn grits cooked,182,1.2,0.2,0.2,0.4,37.9,0.3,4.4,2.1,0,0.6,213.1,0,0.2,0.002,0,0.1,2.1,0.1,0.1,0,0,0.030,0,2.6,0,1.5,18,0.008,51.4,69.4,0.052,0.5,49.7 226 | 224,224,amaranth flakes,134,2.7,0.5,0.8,1,27,0.2,5.9,3.6,0,0.067,1.1,0,0.033,0.025,0,0.000,1,0,0.006,1,0,0.5,0.007,6.5,0.011,0.7,9.5,0,125.8,134.1,0.009,0.011,47.4 227 | 225,225,granola,293,14.6,2.4,6.6,4.7,32.3,11.9,8.2,5.3,0,0.010,3.5,0.008,0.3,0.017,0,0.2,1.6,0.9,0.2,0.7,0,6.7,0.035,45.6,0.4,2.4,100.8,2.4,258.6,323.4,0.087,2.5,109.142 228 | 226,226,muesli with fruit nuts,231,3.4,0.5,1.2,1.4,45.7,14.2,4.3,5.8,0,0.1,5.1,0.2,0.4,177.6,0.047,0.5,5.5,2.8,0.5,0,0.028,1.4,0.038,25.8,0.2,5.9,72,1.3,176.4,229.2,0.083,1.6,91.1 229 | 227,227,muesli master crumble,124,2.4,1,19.8,8.5,4.5,0.042,2.4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,9.3 230 | 228,228,bran flakes,131,0.8,0.2,0.1,0.5,32.2,7.4,4,7.3,0,0.2,1.4,0,0.3,0.5,0.4,0.011,0.6,6.7,0.3,0.7,0,0.090,0.3,0.025,17.6,0.3,11.2,91.6,1.4,179.6,213.2,0.083,45.358 231 | 229,229,nut cereal,245,5.9,1,3.9,0.5,46.2,17.2,4,3.6,0,0.3,2.6,0.2,0.5,0.3,0,0.6,6.5,0.3,0.7,10.7,0.002,0,0,75.6,0.2,10.3,39,1,106.8,134.4,0.066,3,156.5 232 | 230,230,corn flakes,108,0.3,0.098,0.071,0.018,24.6,2.2,1.7,0.8,0,0.2,0.9,0,0.3,1.3,0.026,0.056,0.5,5.9,0.022,0.5,18.2,0.011,0.011,0,0.6,0.083,5.4,2,0.060,9.2,30,0.077,27.947 233 | 231,231,corn grits dry,36,0.2,0.022,0.018,0.032,7.7,0.008,0.7,0.4,0,0.054,1.1,0,0.1,0.080,0,0.032,0.4,0.072,0.097,0,0,0.070,0,0.4,0.086,0.3,3.5,0.029,10.8,13.7,0.024,0.046,9.7 234 | -------------------------------------------------------------------------------- /examples/food_data_set/README.md: -------------------------------------------------------------------------------- 1 | # Food Data Set 2 | 3 | Data files downloaded from https://www.kaggle.com/datasets/utsavdey1410/food-nutrition-dataset, published 4 | under Creative Commons license 0: Public Domain. 5 | -------------------------------------------------------------------------------- /examples/food_data_set/food_data_merge.py: -------------------------------------------------------------------------------- 1 | # 2 | # food_data_merge.py 3 | # 4 | # Work with data files describing nutritional content of common foods, 5 | # downloaded from https://www.kaggle.com/datasets/utsavdey1410/food-nutrition-dataset. 6 | # 7 | # Rename columns to be valid Python identifiers, and re-rank so that 8 | # id's are unique. 9 | # 10 | 11 | from pathlib import Path 12 | 13 | import littletable as lt 14 | 15 | data_dir = Path(__file__).parent 16 | 17 | # read all CSVs into a single table - multiple calls to csv_import 18 | # on the same Table will concatenate all into a single group 19 | foods = lt.Table() 20 | for data_file in data_dir.glob("FOOD-DATA-GROUP*.csv"): 21 | foods.csv_import(data_file, transforms={"*": lt.Table.convert_numeric}) 22 | 23 | # re-number food items into an "id" field 24 | foods.rank("id") 25 | 26 | # convert field names with capital letters and spaces to snake case, 27 | # to make them easier to work with as valid Python identifiers 28 | field_names = foods.info()["fields"] 29 | py_names = [nm.lower().replace(" ", "_") for nm in field_names] 30 | for py_name, field_name in zip(py_names, field_names): 31 | if not py_name: 32 | continue 33 | foods.compute_field(py_name, field_name) 34 | 35 | # change the initial field name from "" to "id", and remove "unnamed" field 36 | py_names[0] = "id" 37 | py_names.remove("unnamed:_0") 38 | 39 | # build single CSV file from all data 40 | foods.select(py_names).csv_export(data_dir / "combined_food_data.csv") 41 | -------------------------------------------------------------------------------- /examples/full_text_search.py: -------------------------------------------------------------------------------- 1 | # 2 | # full_text_search.py 3 | # 4 | # Example code using littletable with a search index to do full text searching 5 | # of a table attribute. 6 | # 7 | # Copyright (c) 2020 Paul T. McGuire 8 | # 9 | 10 | import littletable as lt 11 | import textwrap 12 | 13 | # create table of recipes, by title and ingredients 14 | recipe_data = textwrap.dedent( 15 | """\ 16 | title,ingredients 17 | Tuna casserole,tuna noodles cream of mushroom soup 18 | Hawaiian pizza,pizza dough pineapple ham tomato sauce 19 | Margherita pizza,pizza dough cheese pesto artichoke hearts 20 | Pepperoni pizza,pizza dough cheese tomato sauce pepperoni 21 | Grilled cheese sandwich,bread cheese butter 22 | Tuna melt,tuna mayonnaise tomato bread cheese 23 | Chili dog,hot dog chili onion bun 24 | French toast,egg milk vanilla bread maple syrup 25 | BLT,bread bacon lettuce tomato mayonnaise 26 | Reuben sandwich,rye bread sauerkraut corned beef swiss cheese russian dressing thousand island 27 | Hamburger,ground beef bun lettuce ketchup mustard pickle 28 | Cheeseburger,ground beef bun lettuce ketchup mustard pickle cheese 29 | Bacon cheeseburger,ground beef bun lettuce ketchup mustard pickle cheese bacon 30 | """ 31 | ) 32 | recipes = lt.Table().csv_import(recipe_data) 33 | 34 | # define search index on "ingredients" attribute 35 | search_attr = "ingredients" 36 | recipes.create_search_index(search_attr) 37 | 38 | # run sample queries 39 | queries = """\ 40 | tuna 41 | tuna +cheese 42 | pineapple +bacon lettuce beef -sauerkraut tomato 43 | pizza dough -pineapple 44 | pizza dough --pineapple 45 | bread bacon 46 | bread ++bacon""".splitlines() 47 | 48 | # run each query, listing top 5 matches 49 | for query in queries: 50 | query = query.strip() 51 | if not query: 52 | continue 53 | 54 | matches = recipes.search.ingredients(query, limit=5, min_score=-100000) 55 | 56 | print(query) 57 | if matches: 58 | for rec in matches: 59 | print(" -", rec.title, rec.ingredients_search_score) 60 | else: 61 | print(" ") 62 | print() 63 | 64 | # redo last match, getting the words for each match 65 | print("repeat last match, including matched words") 66 | matches = recipes.search.ingredients( 67 | query, limit=5, min_score=-100000, include_words=True 68 | ) 69 | print(query) 70 | for rec in matches: 71 | print(" -", rec.title,rec.ingredients_search_score, rec.ingredients_search_words) 72 | 73 | # build a more complex search index, using words from multiple fields 74 | recipes.create_search_index("recipe_terms", using="ingredients title") 75 | matches = recipes.search.recipe_terms("casserole Hawaiian", as_table=True) 76 | matches.present(fields="title ingredients".split()) 77 | 78 | # exception gets raised if search() is called after the table has been modified without 79 | # rebuilding the search index 80 | print("\nmodify source table to show exception raised when search index is no longer valid") 81 | recipes.pop(0) 82 | matches = recipes.search.ingredients(query) 83 | -------------------------------------------------------------------------------- /examples/littletable_demo.py: -------------------------------------------------------------------------------- 1 | # 2 | # littletable_demo.py 3 | # 4 | # Copyright 2010, Paul T. McGuire 5 | # 6 | from collections import namedtuple 7 | import littletable as lt 8 | 9 | # load catalog of products 10 | catalog = lt.Table("ACME catalog") 11 | catalog.create_index("sku", unique=True) 12 | catalog_data = """\ 13 | sku,descr,unitofmeas,unitprice 14 | BRDSD-001,Bird seed,LB,3 15 | BBS-001,Steel BB's,LB,5 16 | MGNT-001,Magnet,EA,8 17 | MAGLS-001,Magnifying glass,EA,12 18 | ANVIL-001,1000lb anvil,EA,100 19 | ROPE-001,1 in. heavy rope,100FT,10 20 | ROBOT-001,Domestic robot,EA,5000""" 21 | catalog.csv_import(catalog_data, transforms={"unitprice": int}) 22 | catalog.present() 23 | 24 | print(catalog.by.sku["ANVIL-001"].descr) 25 | catalog.create_index("unitofmeas") 26 | catalog.by.unitofmeas["EA"].present() 27 | 28 | # load table of customers 29 | Customer = namedtuple("Customer", "id name") 30 | customers = lt.Table("customers") 31 | customers.create_index("id", unique=True) 32 | customer_data = """\ 33 | id,name 34 | 0010,George Jetson 35 | 0020,Wile E. Coyote 36 | 0030,Jonny Quest""" 37 | customers.csv_import(customer_data, row_class=Customer) 38 | customers.present() 39 | 40 | # load wishlist items for each customer 41 | wishitems = lt.Table("wishitems") 42 | wishitems.create_index("custid") 43 | wishitems.create_index("sku") 44 | 45 | # there is no user-defined type for these items, the default is SimpleNamespace 46 | wishlist_data = """\ 47 | custid,sku 48 | 0030,MAGLS-001 49 | 0020,MAGLS-001 50 | 0020,ANVIL-001 51 | 0020,ROPE-001 52 | 0020,BRDSD-001 53 | 0020,BBS-001 54 | 0020,MAGNT-001 55 | 0030,MAGNT-001 56 | 0030,ROBOT-001 57 | 0010,ROBOT-001""" 58 | wishitems.csv_import(wishlist_data) 59 | 60 | # print a particular customer name 61 | print(customers.by.id["0030"].name) 62 | print() 63 | 64 | # print all items sold by the pound 65 | for item in catalog.where(unitofmeas="LB"): 66 | print(item.sku, item.descr) 67 | print() 68 | 69 | # if querying on an indexed item, use ".by.attribute-name[key]" 70 | # catalog.create_index("unitofmeas") 71 | for item in catalog.by.unitofmeas["LB"]: 72 | print(item.sku, item.descr) 73 | print() 74 | 75 | # print all items that cost more than 10 76 | for item in catalog.where(lambda ob: ob.unitprice > 10): 77 | print(item.sku, item.descr, item.unitprice) 78 | print() 79 | 80 | # join tables to create queryable wishlists collection - the following are all equivalent 81 | wishlists = (customers.join_on("id") + wishitems.join_on("custid")).join_on("sku") + catalog.join_on("sku") 82 | wishlists = (customers.join_on("id") + wishitems.join_on("custid")).join_on("sku") + catalog 83 | wishlists = catalog + (customers.join_on("id") + wishitems.join_on("custid")).join_on("sku") 84 | wishlists = catalog.join_on("sku") + (customers.join_on("id") + wishitems.join_on("custid")) 85 | wishlists = customers.join_on("id") + wishitems.join_on("custid") + catalog.join_on("sku") 86 | print(wishlists().table_name) 87 | print(wishlists()("wishlists").table_name) 88 | 89 | # print all wishlist items with price > 10 (use Table.gt instead of lambda) 90 | # bigticketitems = wishlists().where(lambda ob : ob.unitprice > 10) 91 | bigticketitems = wishlists().where(unitprice=lt.Table.gt(10)) 92 | for bti in bigticketitems: 93 | print(bti) 94 | print() 95 | 96 | # list all wishlist items by customer, then in descending order by unit price 97 | for item in wishlists().orderby("custid, unitprice desc"): 98 | print(item) 99 | print() 100 | 101 | # display formatted tabular output 102 | wishlists().orderby("custid, unitprice desc")("Wishlists").select( 103 | "custid name sku descr" 104 | ).present(groupby="custid name") 105 | 106 | # create simple pivot table, grouping wishlist data by customer name 107 | wishlistsdata = wishlists() 108 | wishlistsdata.create_index("name") 109 | pivot = wishlistsdata.pivot("name") 110 | pivot.dump(row_fn=lambda o: "%s %s" % (o.sku, o.descr)) 111 | print() 112 | 113 | # pivot on both sku number and customer name, giving tabular output 114 | piv2 = wishlistsdata.pivot("sku name") 115 | piv2.dump_counts() 116 | print() 117 | 118 | # pivot on both sku number and customer name, giving tabular output 119 | # tabulate by sum(unitprice) for all items in each pivot table cell 120 | piv2.dump_counts(count_fn=lambda recs: sum(r.unitprice for r in recs)) 121 | print() 122 | -------------------------------------------------------------------------------- /examples/nfkc_normalization.py: -------------------------------------------------------------------------------- 1 | # NFKC_normalization.py 2 | # 3 | # List out Unicode characters that normalize to the ASCII character set. 4 | # 5 | # Copyright Paul McGuire, 2022 6 | # 7 | import sys 8 | import unicodedata 9 | 10 | import littletable as lt 11 | 12 | ALL_UNICODE_CHARS = False 13 | 14 | # initialize accumulator for ASCII character to collect Unicode characters 15 | # that normalize back to ASCII characters that can be used in Python 16 | # identifiers (A-Z, a-z, 0-9, _, and ·) 17 | _· = "_·" 18 | ident_chars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" 19 | "abcdefghijklmnopqrstuvwxyz" 20 | "0123456789" + _·) 21 | accum = {ch: [] for ch in ident_chars} 22 | 23 | # build up accumulator by walking Unicode range 24 | unicode_upper_limit = sys.maxunicode+1 if ALL_UNICODE_CHARS else 65536 25 | for i in range(32, unicode_upper_limit): 26 | ch = chr(i) 27 | norm = unicodedata.normalize("NFKC", ch) 28 | if norm in accum: 29 | accum[norm].append(ch) 30 | 31 | # convert accumulator to a littletable Table for presentation 32 | normalizations = lt.Table() 33 | for asc_char, normalizing_chars in accum.items(): 34 | normalizations.insert_many( 35 | {"ASCII": asc_char, 36 | "ord": ord(asc_char), 37 | "Unicode": norm_char, 38 | "code_point": ord(norm_char), 39 | "name": unicodedata.name(norm_char), 40 | } 41 | for norm_char in normalizing_chars 42 | ) 43 | normalizations.orderby("ASCII") 44 | normalizations.present(groupby="ASCII ord") 45 | -------------------------------------------------------------------------------- /examples/peps.json.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/examples/peps.json.zip -------------------------------------------------------------------------------- /examples/peps.py: -------------------------------------------------------------------------------- 1 | # 2 | # peps.py 3 | # 4 | # Using PEP data extracted from Python.org, this program demonstrates 5 | # pulling data from a compressed JSON file, and accessing PEP entries 6 | # by id, year created, and text search of the PEP abstracts. 7 | # 8 | # Copyright Paul McGuire, 2022 9 | # 10 | import json 11 | from operator import attrgetter 12 | from pathlib import Path 13 | import littletable as lt 14 | 15 | 16 | # import PEP data from JSON, converting id's to ints and created 17 | # date stings to Python datetimes 18 | peps = lt.Table().json_import( 19 | Path(__file__).parent / "peps.json.zip", 20 | transforms={ 21 | "id": int, 22 | "created": lt.Table.parse_date("%d-%b-%Y"), 23 | } 24 | ) 25 | 26 | # print metadata of imported records 27 | print(peps.info()) 28 | 29 | # access records by unique PEP id 30 | peps.create_index("id", unique=True) 31 | print("PEP20:", peps.by.id[20].title) 32 | 33 | # add a numeric "year" field, and index it 34 | get_year = attrgetter("created.year") 35 | peps.compute_field("year", get_year) 36 | peps.create_index("year") 37 | 38 | # present PEPs created in 2016 39 | peps.by.year[2016]("PEPs Created in 2016").select("id python_version title status url").present() 40 | 41 | # how many PEPs since 2020? 42 | print("Number of PEPs since 2020", len(peps.by.year[2020:])) 43 | print() 44 | 45 | # pivot by year and dump counts, or present as nice table 46 | peps.pivot("year").dump_counts() 47 | peps.pivot("year").as_table().present() 48 | 49 | # create full text search on PEP abstracts 50 | peps.create_search_index("abstract_terms", using="abstract title authors status year") 51 | 52 | # search for PEPs referring to the walrus operator 53 | walrus_pep = peps.search.abstract_terms("walrus", as_table=True)("'walrus' Search Results") 54 | walrus_pep.select("id title year authors").present() 55 | print(walrus_pep.select("id title year authors").json_export()) 56 | 57 | # search for PEPs referring to GvR or Guido or BDFL 58 | bdfl_peps = peps.search.abstract_terms("gvr guido bdfl", as_table=True)("GvR PEPs") 59 | bdfl_peps.orderby("id") 60 | bdfl_peps.select("id title year url authors").present() 61 | 62 | 63 | # define a custom JSON encoder for datetime.date field 64 | class JsonDateEncoder(json.JSONEncoder): 65 | def default(self, o): 66 | import datetime 67 | if isinstance(o, datetime.date): 68 | return str(o) 69 | return super().default(o) 70 | 71 | 72 | print(bdfl_peps.select("id title created").json_export(json_encoder=(JsonDateEncoder,))) 73 | -------------------------------------------------------------------------------- /examples/pivot_demo.py: -------------------------------------------------------------------------------- 1 | # 2 | # pivot_demo.py 3 | # 4 | # Using littletable, performs summaries on one and two attribute pivots 5 | # of a table of US populated place names. 6 | # 7 | # Uses data excerpted from the US.TXT geographic names data file, provided 8 | # under Creative Commons Attribution 3.0 License by GeoNames (www.geonames.org) 9 | # 10 | # Copyright (c) 2011, 2024 Paul T. McGuire 11 | # 12 | import littletable as lt 13 | from pathlib import Path 14 | 15 | 16 | this_dir = Path(__file__).parent 17 | 18 | # import from csv, convert elevation from meters to feet 19 | places: lt.Table = lt.csv_import( 20 | this_dir / "us_ppl.zip", 21 | transforms={ 22 | "elev": lambda s: int(s) * 33 / 10, 23 | "pop": int 24 | }, 25 | ) 26 | print(places.info()) 27 | 28 | # add computed field, elevation rounded down by 1000's 29 | places.compute_field("elev_000", lambda x: int(x.elev / 1000) * 1000, 0) 30 | 31 | # create indexes for use by pivots 32 | places.create_index("state") 33 | places.create_index("elev_000") 34 | 35 | print("summarize population by state") 36 | piv = places.pivot("state") 37 | ppl_by_state = piv.as_table(sum, "pop").orderby("pop desc") 38 | for rec in ppl_by_state: 39 | print(rec.state, rec.pop) 40 | 41 | print() 42 | piv.dump_counts(count_fn=lambda recs: sum(r.pop for r in recs)) 43 | 44 | print() 45 | print("summarize population by elevation") 46 | piv = places.pivot("elev_000") 47 | ppl_by_elev = piv.as_table(sum, "pop") 48 | for rec in ppl_by_elev: 49 | print(rec.elev_000, rec.pop) 50 | 51 | print() 52 | print("summarize population by state and elevation") 53 | piv = places.pivot("state elev_000") 54 | 55 | # dump all the sum of all population attributes for each row in each subtable 56 | piv.dump_counts(count_fn=lambda recs: sum(r.pop for r in recs)) 57 | print() 58 | 59 | # select a subtable from the pivot table 60 | sd0 = piv["SD"][0]("SD places below 1000 ft elev") 61 | sd0.select("name pop elev elev_000").present() 62 | print() 63 | 64 | # pplByElev = piv.as_table(sum, "pop") 65 | # for rec in pplByElev[:100]: 66 | # print(rec.state, rec.elev_000, rec.pop) 67 | 68 | # find average elevation of person by state 69 | print("Average elevation of each person by state") 70 | piv = places.pivot("state") 71 | piv.dump_counts( 72 | count_fn=lambda recs: int( 73 | sum(r.pop * r.elev for r in recs) / sum(r.pop for r in recs) 74 | ) 75 | ) 76 | 77 | low_liers = places.where(elev=lt.Table.le(20)) 78 | print(f"\nPopulation at or below 20 feet sea level: {sum(low_liers.all.pop):,}") -------------------------------------------------------------------------------- /examples/present_groupby_example.py: -------------------------------------------------------------------------------- 1 | import littletable as lt 2 | 3 | tbl = lt.Table("Academy Awards 1960-1969").csv_import("""\ 4 | year,award,movie,recipient 5 | 1960,Best Picture,Ben-Hur, 6 | 1960,Best Actor,Ben-Hur,Charlton Heston 7 | 1960,Best Actress,The Heiress,Simone Signoret 8 | 1960,Best Director,Ben-Hur,William Wyler 9 | 1961,Best Picture,The Apartment, 10 | 1961,Best Actor,Elmer Gantry,Burt Lancaster 11 | 1961,Best Actress,Butterfield 8,Elizabeth Taylor 12 | 1961,Best Director,The Apartment,Billy Wilder 13 | 1962,Best Picture,West Side Story, 14 | 1962,Best Actor,Judgment at Nuremberg,Maximilian Schell 15 | 1962,Best Actress,Two Women,Sophia Loren 16 | 1962,Best Director,West Side Story,Willian Wise/Jerome Robbins 17 | 1963,Best Picture,Lawrence of Arabia, 18 | 1963,Best Actor,To Kill A Mockingbird,Gregory Peck 19 | 1963,Best Actress,The Miracle Worker,Anne Bancroft 20 | 1963,Best Director,Lawrence of Arabia,David Lean 21 | 1964,Best Picture,Tom Jones, 22 | 1964,Best Actor,Lilies of the Field,Sidney Poitier 23 | 1964,Best Actress,Hud,Patricia Neal 24 | 1964,Best Director,Tom Jones,Tony Richardson 25 | 1965,Best Picture,My Fair Lady, 26 | 1965,Best Actor,My Fair Lady,Rex Harrison 27 | 1965,Best Actress,Mary Poppins,Julie Andrews 28 | 1965,Best Director,My Fair Lady,George Kukor 29 | 1966,Best Picture,The Sound of Music, 30 | 1966,Best Actor,Cat Ballou,Lee Marvin 31 | 1966,Best Actress,Darling,Julie Christie 32 | 1966,Best Director,The Sound of Music,Robert Wise 33 | 1967,Best Picture,A Man for All Season, 34 | 1967,Best Actor,A Man for All Seasons,Paul Scofield 35 | 1967,Best Actress,Who's Afraid of Virginia Woolf,Elizabeth Taylor 36 | 1967,Best Director,A Man for All Seasons,Fred Zinnemann 37 | 1968,Best Picture,In The Heat of The Night, 38 | 1968,Best Actor,In The Heat of The Night,Rod Steiger 39 | 1968,Best Actress,Guess Who's Coming to Dinner,Katherine Hepburn 40 | 1968,Best Director,The Graduate,Mike Nichols 41 | 1969,Best Picture,Oliver!, 42 | 1969,Best Actor,Charly,Cliff Robertson 43 | 1969,Best Actress,Funny Girl,Barbra Streisand 44 | 1969,Best Director,Oliver!,Carol Reed 45 | """) 46 | 47 | tbl.present(groupby="year") 48 | -------------------------------------------------------------------------------- /examples/pyscript/matplotlib.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | Pyscript + littletable Demo 15 | 16 | 17 | 18 | 19 | packages = [ 20 | "matplotlib", 21 | "littletable", 22 | ] 23 | 24 | 25 | 26 | 27 | 34 | 114 | 115 |
116 |     

117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/sliced_indexing.py: -------------------------------------------------------------------------------- 1 | # sliced_indexing.py 2 | # 3 | # Demonstration of using slices on indexed attributes to do range filtering. 4 | # Even non-integer types are supported (as long as they support >= and < 5 | # operations). 6 | # 7 | import littletable as lt 8 | import textwrap 9 | import datetime 10 | 11 | 12 | def main(): 13 | 14 | # read data from US place names CSV file 15 | transforms = { 16 | 'pop': int, 17 | # convert from meters to feet 18 | 'elev': lambda s: int(float(s) * 3.28084), 19 | 'lat': float, 20 | 'long': float, 21 | } 22 | us_ppl = lt.Table().csv_import( 23 | "./us_ppl.zip", 24 | transforms=transforms 25 | ).select("id name state elev lat long pop") 26 | 27 | print(us_ppl.info()) 28 | us_ppl.create_index("elev") 29 | 30 | test = "elev < 0" 31 | us_ppl.by.elev[:0](f"{test} (sliced)")[:100].present() 32 | 33 | test = "elev >= 1000" 34 | us_ppl.by.elev[1000:](f"{test} (sliced)").orderby("elev desc")[:100].present() 35 | 36 | test = "0 <= elev < 100" 37 | us_ppl.by.elev[0:100](f"{test} (sliced)")[:100].present() 38 | 39 | # slice using non-integer types 40 | 41 | us_ppl.create_index("name") 42 | a_ppl_slice = us_ppl.by.name["A":"C"] 43 | a_ppl_slice[:100].present() 44 | 45 | a_ppl_slice = us_ppl.by.name["Z":] 46 | a_ppl_slice[:100].present() 47 | 48 | # load some sales data from Acme, Inc. 49 | sales_data = textwrap.dedent("""\ 50 | date,customer,sku,qty 51 | 2000/01/01,0020,ANVIL-001,1 52 | 2000/01/01,0020,BRDSD-001,5 53 | 2000/02/15,0020,BRDSD-001,5 54 | 2000/03/31,0020,BRDSD-001,5 55 | 2000/03/31,0020,MAGNT-001,1 56 | 2000/04/01,0020,ROBOT-001,1 57 | 2000/04/15,0020,BRDSD-001,5 58 | 1900/02/29,0020,BRDSD-001,5 59 | """) 60 | 61 | # load data from CSV, converting dates to datetime.date 62 | transforms = {'date': lt.Table.parse_date("%Y/%m/%d"), 63 | 'qty': int} 64 | sales = lt.Table("All 2000 Sales").csv_import( 65 | sales_data, 66 | transforms=transforms, 67 | ) 68 | sales.present() 69 | 70 | # get sales from the first quarter only 71 | sales.create_index("date") 72 | jan_01 = datetime.date(2000, 1, 1) 73 | apr_01 = datetime.date(2000, 4, 1) 74 | first_qtr_sales = sales.by.date[jan_01: apr_01]("2000 Q1 Sales") 75 | first_qtr_sales.present() 76 | 77 | # load data from CSV, leave dates as strings (still sortable when in YYYY/MM/DD form) 78 | transforms = {'qty': int} 79 | sales = lt.Table("All 2000 Sales").csv_import( 80 | sales_data, 81 | transforms=transforms, 82 | ) 83 | 84 | # get sales from the first quarter only 85 | sales.create_index("date") 86 | first_qtr_sales = sales.by.date["2000/01/01": "2000/04/01"]("2000 Q1 Sales") 87 | first_qtr_sales.present() 88 | 89 | 90 | if __name__ == '__main__': 91 | main() 92 | -------------------------------------------------------------------------------- /examples/star_trek_tos.py: -------------------------------------------------------------------------------- 1 | import littletable as lt 2 | 3 | st = lt.Table().csv_import( 4 | "star_trek_tos_eps.csv", 5 | transforms={"rating": float, "votes": int} 6 | ) 7 | 8 | # sort by rating, and add "rank" field 9 | st.orderby(["rating desc", "votes desc"]) 10 | st.rank("rank") 11 | 12 | # display 10 best and worst episodes 13 | fields = "rank date title rating votes".split() 14 | count = 10 15 | best_and_worst = (st[:count] + st[-count:])(f"{count} Best and Worst Star Trek Episodes") 16 | best_and_worst.present( 17 | fields, 18 | caption="data from IMDB.com", 19 | caption_justify="right" 20 | ) 21 | 22 | # full-text search of descriptions 23 | st.create_search_index("description") 24 | for query in [ 25 | "--kirk ++spock", 26 | "--kirk ++mccoy", 27 | ]: 28 | st.search.description(query, as_table=True).select("date title description").present() 29 | -------------------------------------------------------------------------------- /examples/star_trek_tos_eps.csv: -------------------------------------------------------------------------------- 1 | date,description,href,rating,season,title,votes 2 | 1988-11-27,Capt. Pike is held prisoner and tested by aliens who have the power to project incredibly lifelike illusions.,/title/tt0059753/?ref_=ttep_ep1,7.7,1,The Cage,5100 3 | 1966-09-08,Dr. McCoy discovers his old flame is not what she seems after crew members begin dying from a sudden lack of salt in their bodies.,/title/tt0708469/?ref_=ttep_ep2,7.3,1,The Man Trap,3892 4 | 1966-09-15,Captain Kirk must learn the limits to the power of a 17-year-old boy with the psychic ability to create anything and destroy anyone.,/title/tt0708424/?ref_=ttep_ep3,7.1,1,Charlie X,3670 5 | 1966-09-22,The flight recorder of the 200-year-old U.S.S. Valiant relays a tale of terror--a magnetic storm at the edge of the galaxy!,/title/tt0061027/?ref_=ttep_ep4,7.8,1,Where No Man Has Gone Before,4570 6 | 1966-09-29,The crew is infected with a mysterious disease that removes people's emotional inhibitions to a dangerous degree.,/title/tt0708473/?ref_=ttep_ep5,7.9,1,The Naked Time,3445 7 | 1966-10-06,"A transporter malfunction splits Captain Kirk into two halves: one meek and indecisive, the other violent and ill tempered. The remaining crew members stranded on the planet cannot be beamed up to the ship until a problem is fixed.",/title/tt0708463/?ref_=ttep_ep6,7.7,1,The Enemy Within,3282 8 | 1966-10-13,The Enterprise picks up untrustworthy entrepreneur Harry Mudd accompanied by three beautiful women who immediately put a spell on all the male crew members.,/title/tt0708439/?ref_=ttep_ep7,6.8,1,Mudd's Women,3210 9 | 1966-10-20,Nurse Chapel is reunited with her fianc�; but his new obsession leads him to make an android duplicate of Captain Kirk.,/title/tt0708486/?ref_=ttep_ep8,7.5,1,What Are Little Girls Made Of?,3056 10 | 1966-10-27,"The Enterprise discovers a planet exactly like Earth, but the only inhabitants are children who contract a fatal disease upon entering puberty.",/title/tt0394905/?ref_=ttep_ep9,7.1,1,Miri,3038 11 | 1966-11-03,Kirk and psychiatrist Helen Noel are trapped on a maximum security penal colony that experiments with mind control and Spock must use the Vulcan mind-meld to find a way to save them.,/title/tt0708426/?ref_=ttep_ep10,7.5,1,Dagger of the Mind,2870 12 | 1966-11-10,"After the Enterprise is forced to destroy a dangerous marker buoy, a gigantic alien ship arrives to capture and condemn the crew as trespassers.",/title/tt0708458/?ref_=ttep_ep11,8.1,1,The Corbomite Maneuver,3219 13 | 1966-11-17,"Spock kidnaps the crippled Capt. Pike, hijacks the Enterprise and then surrenders for court martial.",/title/tt0394904/?ref_=ttep_ep12,8.4,1,The Menagerie: Part I,3171 14 | 1966-11-24,"At Spock's court martial, he explains himself with mysterious footage about when Capt. Pike was kidnapped by powerful illusion casting aliens.",/title/tt0708472/?ref_=ttep_ep13,8.3,1,The Menagerie: Part II,3073 15 | 1966-12-08,"While Captain Kirk investigates whether an actor is actually a presumed dead mass murderer, a mysterious assailant is killing the people who could identify the fugitive.",/title/tt0394903/?ref_=ttep_ep14,7.2,1,The Conscience of the King,2789 16 | 1966-12-15,The Enterprise must decide on its response when a Romulan ship makes a destructively hostile armed probe of Federation territory.,/title/tt0708420/?ref_=ttep_ep15,8.9,1,Balance of Terror,3710 17 | 1966-12-29,"The past three months has left the crew of the Enterprise exhausted and in desperate need of a break, but does this explain McCoy's encounter with a human-sized white rabbit or Kirk crossing paths with the prankster who plagued his days at Starfleet Academy?",/title/tt0708446/?ref_=ttep_ep16,7.6,1,Shore Leave,2957 18 | 1967-01-05,"The Galileo, under Spock's command, crash-lands on a hostile planet. As the Enterprise races against time to find the shuttlecraft, Spock's strictly logical leadership clashes with the fear and resentment of his crew.",/title/tt0708465/?ref_=ttep_ep17,7.7,1,The Galileo Seven,2834 19 | 1967-01-12,A being that controls matter and creates planets wants to play with the Enterprise crew.,/title/tt0708478/?ref_=ttep_ep18,7.4,1,The Squire of Gothos,2770 20 | 1967-01-19,"For bringing hostility into their solar system, a superior alien race brings Captain Kirk in mortal combat against the reptilian captain of an alien ship he was pursuing.",/title/tt0708418/?ref_=ttep_ep19,8.0,1,Arena,3005 21 | 1967-01-26,The Enterprise is thrown back in time to 1960s Earth.,/title/tt0708484/?ref_=ttep_ep20,8.0,1,Tomorrow Is Yesterday,2854 22 | 1967-02-02,Kirk draws a court martial in the negligent death of a crewman.,/title/tt0708425/?ref_=ttep_ep21,7.5,1,Court Martial,2650 23 | 1967-02-09,"Seeking the answer to a century-old mystery, Kirk and crew encounter a vacantly peaceful society under a 6000-year autocratic rule that kills all those it can't absorb.",/title/tt0708476/?ref_=ttep_ep22,7.2,1,The Return of the Archons,2573 24 | 1967-02-16,"While on patrol in deep space, Captain Kirk and his crew find and revive a genetically engineered world conqueror and his compatriots from Earth's 20th century.",/title/tt0708447/?ref_=ttep_ep23,8.8,1,Space Seed,3647 25 | 1967-02-23,Kirk and Spock must save their ship's crew when they are declared all killed in action in a bizarre computer simulated war where the actual deaths must occur to continue.,/title/tt0708414/?ref_=ttep_ep24,8.1,1,A Taste of Armageddon,2773 26 | 1967-03-02,"The Enterprise investigates a planet whose colonists should be dead, but are not.",/title/tt0708483/?ref_=ttep_ep25,8.0,1,This Side of Paradise,2712 27 | 1967-03-09,"The Enterprise is sent to a mining colony that is being terrorized by a mysterious monster, only to find that the situation is not that simple.",/title/tt0708460/?ref_=ttep_ep26,8.4,1,The Devil in the Dark,2925 28 | 1967-03-23,"With a war with Klingons raging, Kirk and Spock attempt to resist an occupation of a planet with incomprehensibly placid natives.",/title/tt0708429/?ref_=ttep_ep27,8.2,1,Errand of Mercy,2668 29 | 1967-03-30,"Existence itself comes under threat from a man's power-struggle with his alternate self, with the Enterprise's strained dilithium crystals presenting his key to a final solution.",/title/tt0708451/?ref_=ttep_ep28,5.8,1,The Alternative Factor,2601 30 | 1967-04-06,"When a temporarily insane Dr. McCoy accidentally changes history and destroys his time, Kirk and Spock follow him to prevent the disaster, but the price to do so is high.",/title/tt0708455/?ref_=ttep_ep29,9.3,1,The City on the Edge of Forever,4393 31 | 1967-04-13,The Enterprise crew attempts to stop a plague of amoeba-like creatures from possessing human hosts and spreading throughout the galaxy.,/title/tt0708441/?ref_=ttep_ep30,7.6,1,Operation - Annihilate!,2430 32 | 1967-09-15,"In the throes of his Pon Farr mating period, Spock must return to Vulcan to meet his intended future wife, betrothed from childhood.",/title/tt0708416/?ref_=ttep_ep1,8.7,2,Amok Time,3175 33 | 1967-09-22,A powerful being claiming to be the Greek god Apollo appears and demands that the crew of the Enterprise disembark onto his planet to worship him.,/title/tt0708488/?ref_=ttep_ep2,7.2,2,Who Mourns for Adonais?,2674 34 | 1967-09-29,"A powerful artificially intelligent Earth probe, with a murderously twisted imperative, comes aboard the Enterprise and mistakes Capt. Kirk for its creator.",/title/tt0708454/?ref_=ttep_ep3,7.7,2,The Changeling,2422 35 | 1967-10-06,"A transporter accident places Captain Kirk's landing party in an alternate universe, where the Enterprise is in the service of a barbarically brutal empire.",/title/tt0708438/?ref_=ttep_ep4,9.1,2,"Mirror, Mirror",3434 36 | 1967-10-13,"Primitive inhabitants of Gamma Trianguli VI worship a God who orders them to kill visitors, from the Enterprise.",/title/tt0708452/?ref_=ttep_ep5,6.4,2,The Apple,2342 37 | 1967-10-20,The USS Enterprise encounters the wrecked USS Constellation and its distraught commodore who's determined to stop the giant planet-destroying robot ship that killed his crew.,/title/tt0708461/?ref_=ttep_ep6,8.8,2,The Doomsday Machine,3039 38 | 1967-10-27,"Very alien visitors to our galaxy attempt to connect with human consciousness but miss, winding up tapping into the regions of human nightmares instead.",/title/tt0708423/?ref_=ttep_ep7,6.3,2,Catspaw,2379 39 | 1967-11-03,Harry Mudd returns with a plot to take over the Enterprise by stranding the crew on a planet populated by androids under his command.,/title/tt0708432/?ref_=ttep_ep8,7.5,2,"I, Mudd",2480 40 | 1967-11-10,"While returning to the Enterprise aboard the shuttlecraft, Kirk, Spock, McCoy and a seriously ill Federation diplomat find themselves kidnapped by an energized cloud.",/title/tt0708436/?ref_=ttep_ep9,7.3,2,Metamorphosis,2399 41 | 1967-11-17,"The Enterprise hosts a number of quarrelling diplomats, including Spock's father, but someone on board has murder in mind.",/title/tt0708434/?ref_=ttep_ep10,8.6,2,Journey to Babel,2663 42 | 1967-12-01,The Federation clashes with the Klingon Empire over mining rights to Capella IV. A sudden coup between its warrior-minded inhabitants forces Kirk's party to flee with the now dead leader's pregnant wife.,/title/tt0708431/?ref_=ttep_ep11,6.9,2,Friday's Child,2249 43 | 1967-12-08,A landing party from the Enterprise is exposed to strange form of radiation which rapidly ages them.,/title/tt0708459/?ref_=ttep_ep12,7.4,2,The Deadly Years,2256 44 | 1967-12-15,Capt. Kirk obsessively hunts for a mysterious cloud creature he encountered in his youth.,/title/tt0708440/?ref_=ttep_ep13,7.3,2,Obsession,2173 45 | 1967-12-22,Kirk and the Enterprise computer become detectives after Scotty is accused of murdering women on a pleasure planet.,/title/tt0708491/?ref_=ttep_ep14,7.4,2,Wolf in the Fold,2276 46 | 1967-12-29,"To protect a space station with a vital grain shipment, Kirk must deal with Federation bureaucrats, a Klingon battle cruiser and a peddler who sells furry, purring, hungry little creatures as pets.",/title/tt0708480/?ref_=ttep_ep15,8.9,2,The Trouble with Tribbles,3337 47 | 1968-01-05,"Kirk, Uhura and Chekov are trapped on a planet where abducted aliens are enslaved and trained to perform as gladiators for the amusement of bored, faceless aliens.",/title/tt0708466/?ref_=ttep_ep16,7.1,2,The Gamesters of Triskelion,2316 48 | 1968-01-12,The crew of the Enterprise struggles to cope with a planet of imitative people who have modeled their society on 1920s gangsters.,/title/tt0708412/?ref_=ttep_ep17,7.8,2,A Piece of the Action,2534 49 | 1968-01-19,The Enterprise encounters a gigantic energy draining space organism that threatens the galaxy.,/title/tt0708467/?ref_=ttep_ep18,7.6,2,The Immunity Syndrome,2171 50 | 1968-02-02,"Peaceful, primitive peoples get caught up in the struggle between superpowers, with Kirk unhappily trying to restore the balance of power disrupted by the Klingons.",/title/tt0708413/?ref_=ttep_ep19,6.9,2,A Private Little War,2155 51 | 1968-02-09,"The Enterprise is guided to a distant, long-dead world where survivors of an extremely ancient race - existing only as disembodied energy - desiring the bodies of Kirk, Spock and astro-biologist Ann Mulhall so that they may live again.",/title/tt0708445/?ref_=ttep_ep20,7.6,2,Return to Tomorrow,2173 52 | 1968-02-16,"Looking for a missing Federation cultural observer, Kirk and Spock find themselves on a planet whose culture has been completely patterned after Nazi Germany.",/title/tt0708442/?ref_=ttep_ep21,7.6,2,Patterns of Force,2285 53 | 1968-02-23,Galactic alien scouts capture the Enterprise for a return voyage and a prelude to invasion. Kirk's one advantage - they're not used to their adopted human form.,/title/tt0708422/?ref_=ttep_ep22,7.7,2,By Any Other Name,2228 54 | 1968-03-01,"Responding to a distress signal, Kirk finds Captain Tracey of the U.S.S. Exeter violating the prime directive and interfering with a war between the Yangs and the Kohms to find the secret of their longevity.",/title/tt0708474/?ref_=ttep_ep23,6.2,2,The Omega Glory,2251 55 | 1968-03-08,"Kirk and a sub-skeleton crew are ordered to test out an advanced artificially intelligent control system - the M-5 Multitronic system, which could potentially render them all redundant.",/title/tt0708481/?ref_=ttep_ep24,8.1,2,The Ultimate Computer,2280 56 | 1968-03-15,The Enterprise crew investigates the disappearance of a ship's crew on a planet that is a modern version of the Roman Empire.,/title/tt0708421/?ref_=ttep_ep25,7.2,2,Bread and Circuses,2185 57 | 1968-03-29,"While back in time observing Earth in 1968, the Enterprise crew encounters the mysterious Gary Seven who has his own agenda on the planet.",/title/tt0708419/?ref_=ttep_ep26,7.6,2,Assignment: Earth,2428 58 | 1968-09-20,The crew of the Enterprise pursues a mysterious woman who has abducted Spock's brain.,/title/tt0708449/?ref_=ttep_ep1,5.7,3,Spock's Brain,2391 59 | 1968-09-27,An apparently insane Capt. Kirk has the Enterprise deliberately enter the Romulan Neutral Zone where the ship is immediately captured by the enemy.,/title/tt0708464/?ref_=ttep_ep2,8.5,3,The Enterprise Incident,2549 60 | 1968-10-04,"Trapped on a planet whose inhabitants are descended from Northwestern Native Americans, Kirk loses his memory and is proclaimed a God while the crippled Enterprise races back to the planet before it is destroyed by an asteroid.",/title/tt0708475/?ref_=ttep_ep3,6.8,3,The Paradise Syndrome,2209 61 | 1968-10-11,The Enterprise reaches a Federation colony where the adults have all killed themselves but the children play without care.,/title/tt0708417/?ref_=ttep_ep4,5.2,3,And the Children Shall Lead,2250 62 | 1968-10-18,"Lovely telepath Miranda is aide to Ambassador Kollos, in a box to stop insanity when humans see Medusans. She rejects Larry, a designer of Enterprise, and senses murderous intent nearby.",/title/tt0708433/?ref_=ttep_ep5,7.0,3,Is There in Truth No Beauty?,2093 63 | 1968-10-25,"As punishment for ignoring their warning and trespassing on their planet, the Melkot condemn Capt. Kirk and his landing party to the losing side of a surreal recreation of the 1881 historic gunfight at the OK Corral.",/title/tt0708448/?ref_=ttep_ep6,7.3,3,Spectre of the Gun,2216 64 | 1968-11-01,Both humans and Klingons have been lured to a planet by a formless entity that feeds on hatred and has set about to fashion them into a permanent food supply for itself.,/title/tt0708427/?ref_=ttep_ep7,7.9,3,Day of the Dove,2194 65 | 1968-11-08,The Enterprise discovers an apparent asteroid that is on a collision course with a planet is actually an ancient populated generation ship.,/title/tt0708430/?ref_=ttep_ep8,7.2,3,For the World Is Hollow and I Have Touched the Sky,2038 66 | 1968-11-15,"With Capt. Kirk and the derelict USS Defiant apparently lost, the Enterprise grapples with an insanity causing plague and an attack by the Tholians.",/title/tt0708479/?ref_=ttep_ep9,8.1,3,The Tholian Web,2227 67 | 1968-11-22,"After Dr. McCoy helps the leader of a planet populated by people with powerful psionic abilities, they decide to force him to stay by torturing his comrades until he submits.",/title/tt0708443/?ref_=ttep_ep10,6.6,3,Plato's Stepchildren,2116 68 | 1968-11-29,A group of aliens who exist in a state of incredible acceleration invade the Enterprise and abduct Capt. Kirk.,/title/tt0708490/?ref_=ttep_ep11,7.4,3,Wink of an Eye,2034 69 | 1968-12-06,"Trapped in an alien laboratory, Kirk, Spock and McCoy meet an empath and are involved in a series of experiments.",/title/tt0708462/?ref_=ttep_ep12,6.6,3,The Empath,2134 70 | 1968-12-20,"While transporting an arrogant, demanding princess for a political marriage, Captain Kirk must cope both with her biochemical ability to force him to love her and sabotage on his ship.",/title/tt0708428/?ref_=ttep_ep13,7.1,3,Elaan of Troyius,2054 71 | 1969-01-03,"Kirk and Spock are taken prisoners by a former starship captain named Garth, who now resides at, and has taken over, a high security asylum for the criminally insane.",/title/tt0708489/?ref_=ttep_ep14,7.0,3,Whom Gods Destroy,1986 72 | 1969-01-10,The Enterprise encounters two duo-chromatic and mutually belligerent aliens who put the ship in the middle of their old conflict.,/title/tt0708435/?ref_=ttep_ep15,7.3,3,Let That Be Your Last Battlefield,2132 73 | 1969-01-17,Kirk beams down to the planet Gideon and appears to find himself trapped on a deserted Enterprise. Spock on the real Enterprise must use his diplomatic skills to deal with the uncooperative inhabitants of Gideon and find the Captain.,/title/tt0708470/?ref_=ttep_ep16,6.6,3,The Mark of Gideon,1939 74 | 1969-01-24,"After the Enterprise landing party beams down to investigate a geologically interesting planet, their ship is hurled across the galaxy. Kirk and company find a deserted outpost guarded by the deadly image of a beautiful woman.",/title/tt0708450/?ref_=ttep_ep17,6.6,3,That Which Survives,1908 75 | 1969-01-31,"A mysterious, twinkling mass of sapient energy ravages an important archive and Scotty's new girlfriend may be linked to it.",/title/tt0708468/?ref_=ttep_ep18,6.2,3,The Lights of Zetar,1869 76 | 1969-02-14,"On a planet, looking for an urgent medicinal cure, Kirk, Spock and McCoy come across a dignified recluse living privately but in splendor with his sheltered ward and a very protective robot servant.",/title/tt0708444/?ref_=ttep_ep19,7.5,3,Requiem for Methuselah,2105 77 | 1969-02-21,"A group of idealistic hippies, led by an irrational leader, come aboard the U.S.S. Enterprise.",/title/tt0708482/?ref_=ttep_ep20,5.5,3,The Way to Eden,2240 78 | 1969-02-28,Kirk and Spock are caught up in a revolution on a planet where intellectuals and artists live on a utopian city in the sky while the rest of the population toils in mines on the barren surface below.,/title/tt0708456/?ref_=ttep_ep21,7.1,3,The Cloud Minders,1949 79 | 1969-03-07,"Kirk, Spock, Abraham Lincoln and Vulcan legend Surak are pitted in battle against notorious villains from history for the purpose of helping a conscious rock creature's understanding of a concept he does not understand, ""good vs. evil"".",/title/tt0708477/?ref_=ttep_ep22,6.8,3,The Savage Curtain,1977 80 | 1969-03-14,"When Kirk, Spock and McCoy investigate the disappearance of a doomed planet's population, they find themselves trapped in different periods of that world's past.",/title/tt0708415/?ref_=ttep_ep23,8.3,3,All Our Yesterdays,2283 81 | 1969-06-03,Captain Kirk's insane ex-lover Dr. Janice Lester forcibly switches bodies with him in order to take command of the Enterprise.,/title/tt0708485/?ref_=ttep_ep24,6.9,3,Turnabout Intruder,1994 82 | -------------------------------------------------------------------------------- /examples/table_to_dataframe.py: -------------------------------------------------------------------------------- 1 | # 2 | # table_to_dataframe.py 3 | # 4 | # Short example showing how to create a pandas.DataFrame from 5 | # a littletable.Table. 6 | # 7 | import itertools 8 | 9 | import littletable as lt 10 | 11 | 12 | # make a Table 13 | counter = itertools.count() 14 | labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 15 | tbl = lt.Table().insert_many( 16 | (dict(zip("abc", (next(counter), next(counter), next(counter),))) 17 | | {"label": label}) for label in labels 18 | ) 19 | 20 | # pretty output of tbl.info() 21 | lt.Table().insert_many( 22 | {'property': k, 'value': v} for k, v in tbl.info().items() 23 | )("tbl.info()").present() 24 | 25 | # print first 5 rows starting table 26 | tbl[:5].present() 27 | 28 | # create DataFrame using extracted values with fieldnames as columns 29 | df = tbl.as_dataframe() 30 | 31 | # print first 5 rows of df 32 | print(df[:5]) 33 | -------------------------------------------------------------------------------- /examples/time_conversions.py: -------------------------------------------------------------------------------- 1 | # time_conversions.py 2 | import datetime 3 | import textwrap 4 | 5 | import littletable as lt 6 | 7 | process_data = textwrap.dedent("""\ 8 | timestamp,eqpt,event,lot 9 | 2020/01/01 04:15:22,DRILL01,LotStart,PCB146 10 | 2020/01/01 04:16:02,DRILL01,Tool1,PCB146 11 | 2020/01/01 04:19:47,DRILL01,Tool2,PCB146 12 | 2020/01/01 04:26:03,DRILL01,LotEnd,PCB146 13 | """) 14 | 15 | transforms = {'timestamp': lt.Table.parse_datetime("%Y/%m/%d %H:%M:%S")} 16 | 17 | # get timestamp of first event 18 | data = lt.Table().csv_import(process_data, transforms=transforms, limit=1) 19 | start_time = data[0].timestamp 20 | 21 | # read in values, converting timestamps to offset from start time 22 | transforms = {'timestamp': lt.Table.parse_timedelta("%Y/%m/%d %H:%M:%S", reference_time=start_time)} 23 | data = lt.Table(f"Events relative to {start_time}").csv_import(process_data, transforms=transforms) 24 | 25 | data.present() 26 | 27 | 28 | process_data = textwrap.dedent("""\ 29 | elapsed_time,eqpt,event,lot 30 | 0:00:00,DRILL01,LotStart,PCB146 31 | 0:00:40,DRILL01,Tool1,PCB146 32 | 0:03:45,DRILL01,Tool2,PCB146 33 | 0:06:16,DRILL01,LotEnd,PCB146 34 | """) 35 | 36 | transforms = {'elapsed_time': lt.Table.parse_timedelta("%H:%M:%S")} 37 | data = lt.Table(f"Process step elapsed times").csv_import(process_data, transforms=transforms) 38 | 39 | data.present() 40 | -------------------------------------------------------------------------------- /examples/time_zone_db.py: -------------------------------------------------------------------------------- 1 | # 2 | # time_zone_db.py 3 | # 4 | # Read data from a .zip archive containing 2 CSV files, describing 5 | # time zone definitions and country code definitions. 6 | # 7 | # Uses data fetched from https://timezonedb.com/files/TimeZoneDB.csv.zip, 8 | # The database is licensed under Creative Commons Attribution 3.0 License. 9 | # More info at https://timezonedb.com/download 10 | # 11 | from datetime import datetime, timedelta 12 | from pathlib import Path 13 | import sys 14 | 15 | import littletable as lt 16 | 17 | 18 | tzdb_zip_file = Path(__file__).parent / "TimeZoneDB.csv.zip" 19 | 20 | if not tzdb_zip_file.exists(): 21 | print("File not found: Download TimeZoneDB.csv.zip from https://timezonedb.com/files/TimeZoneDB.csv.zip") 22 | sys.exit() 23 | 24 | 25 | # read in all country codes and display the first 20 26 | country_codes = lt.csv_import( 27 | tzdb_zip_file, 28 | zippath="country.csv", 29 | fieldnames="country_code,country_name".split(","), 30 | ) 31 | country_codes[:20].present() 32 | 33 | 34 | def str_to_datetime(s: str) -> datetime: 35 | """ 36 | Function to transform timestamp seconds into a datetime object. 37 | Need special handling since times before Jan 1, 1970 are recorded as 38 | negative values, which datetime.fromtimestamp cannot handle directly. 39 | """ 40 | timestamp = int(s) 41 | if timestamp < 0: 42 | return datetime.fromtimestamp(0) + timedelta(seconds=timestamp) 43 | else: 44 | return datetime.fromtimestamp(timestamp) 45 | 46 | 47 | # read in all timezone definitions and present the first 20 48 | time_zones = lt.csv_import( 49 | tzdb_zip_file, 50 | zippath="time_zone.csv", 51 | fieldnames="zone_name,country_code,abbreviation,time_start,gmt_offset,dst".split(","), 52 | transforms={ 53 | "dst": lambda x: x == "1", 54 | "gmt_offset": int, 55 | "time_start": str_to_datetime 56 | }, 57 | ) 58 | time_zones[:20].present( 59 | width=120, 60 | caption=f"Total {len(time_zones):,} time zone records", 61 | caption_justify="left" 62 | ) 63 | 64 | # query for time zone records for America/Los_Angeles in 2025 65 | start_time = datetime(2025, 1, 1) 66 | end_time = datetime(2025, 12, 31) 67 | time_zones.where( 68 | zone_name="America/Los_Angeles", 69 | time_start=lt.Table.in_range(start_time, end_time) 70 | )("Time zone records for America/Los_Angeles in 2025").present(width=120) 71 | 72 | # how many different time zone names are there? 73 | time_zone_names = list(time_zones.all.zone_name.unique) 74 | print("Total time zone names:", len(time_zone_names)) 75 | -------------------------------------------------------------------------------- /examples/unicode_15.1.0.txt.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/examples/unicode_15.1.0.txt.zip -------------------------------------------------------------------------------- /examples/unicode_320.txt.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/examples/unicode_320.txt.zip -------------------------------------------------------------------------------- /examples/us_ppl.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/examples/us_ppl.zip -------------------------------------------------------------------------------- /examples/zen_of_python_pep20.py: -------------------------------------------------------------------------------- 1 | # 2 | # zen_of_python_pep20.py 3 | # 4 | # read the Zen of Python into a littletable.Table 5 | # 6 | from contextlib import redirect_stdout 7 | import io 8 | import littletable as lt 9 | 10 | # get the Zen of Python content 11 | import_this_stdout = io.StringIO() 12 | with redirect_stdout(import_this_stdout): 13 | import this 14 | import_this_lines = import_this_stdout.getvalue().splitlines()[2:] 15 | 16 | # load into a Table and present 17 | pep20 = lt.Table().insert_many( 18 | {'id': num, 'zen': zen} 19 | for num, zen in enumerate(import_this_lines, start=1) 20 | )("The Zen of Python") 21 | pep20.present() 22 | 23 | # use text search for "better" 24 | pep20.create_search_index("zen") 25 | for entry in pep20.search.zen("better"): 26 | print(entry.zen) 27 | -------------------------------------------------------------------------------- /how_to_use_littletable.md: -------------------------------------------------------------------------------- 1 | How to Use littletable 2 | ====================== 3 | 4 | * [Introduction](#introduction) 5 | * [Creating a table](#creating-a-table) 6 | * [Inserting objects](#inserting-objects) 7 | * [Importing data from CSV files](#importing-data-from-csv-files) 8 | * [Transforming data while importing](#transforming-data-while-importing) 9 | * [Import/export data to Excel files](#importexport-to-excel-files-xlsx) 10 | * [Importing from remote sources using HTTP](#importing-from-remote-sources-using-http) 11 | * [Tabular output](#tabular-output) 12 | * [types.SimpleNamespace](#typessimplenamespace) 13 | * [Removing objects](#removing-objects) 14 | * [Indexing attributes](#indexing-attributes) 15 | * [Querying with indexed attributes](#querying-with-indexed-attributes) 16 | * [Querying for exact matching attribute values](#querying-for-exact-matching-attribute-values) 17 | * [Querying for attribute value ranges](#querying-for-attribute-value-ranges) 18 | * [Range querying on index attributes using slice notation](#range-querying-on-indexed-attributes-using-slice-notation) 19 | * [Grouping a table](#returning-grouped-tables-from-a-table) 20 | * [Splitting a table using a criteria function](#splitting-a-table-using-a-criteria-function) 21 | * [Full-text search on text attributes](#full-text-search-on-text-attributes) 22 | * [Basic statistics on Tables of numeric values](#basic-statistics-on-tables-of-numeric-values) 23 | * [Importing data from fixed-width text files](#importing-data-from-fixed-width-text-files) 24 | * [Joining tables](#joining-tables) 25 | * [Pivoting a table](#pivoting-a-table) 26 | * [Optional dependencies](#optional-dependencies) 27 | * [littletable and pandas](#littletable-and-pandas) 28 | * [littletable and SQLite](#littletable-and-sqlite) 29 | * [Some simple littletable recipes](#some-simple-littletable-recipes) 30 | 31 | 32 | Introduction 33 | ------------ 34 | 35 | `littletable` is a simple Python module to make it easy to work with collections 36 | of objects as if they were records in a database table. `littletable` augments 37 | normal Python list access with: 38 | - indexing by key attributes 39 | - joining multiple tables by common attribute values 40 | - querying for matching objects by one or more attributes 41 | - data pivoting on 1 or more attributes 42 | - full-text search on indexed attributes 43 | - easy CSV import/export (and TSV, JSON, and Excel) 44 | 45 | `littletable` supports storage of: 46 | - user-defined objects 47 | - user-defined objects using `__slots__` 48 | - `dataclasses` 49 | - `collections.namedtuples` and `typing.NamedTuples` 50 | - `types.SimpleNamespaces` 51 | - `dicts` and `typing.TypedDicts` (converted internally to `SimpleNamespaces` for attribute-style access) 52 | - `pydantic` models (including `Model`, `ImmutableModel`, and `ORMModel`) 53 | - `traits` / `traitlets` classes 54 | - `attrs` classes 55 | 56 | It is not necessary to define a table schema for tables in `littletable`; the 57 | schema of the data emerges from the attributes of the stored objects, and those 58 | used to define indexes and queries. 59 | 60 | Indexes can be created and dropped at any time. An index can be defined to have 61 | unique or non-unique key values, and whether or not to allow null values. 62 | 63 | Instead of returning DataSets or rows of structured values, `littletable` queries 64 | return new Tables. This makes it easy to arrive at a complex query by a sequence 65 | of smaller steps. The resulting values are also easily saved to a CSV file, 66 | like any other `littletable` table. 67 | 68 | 69 | Creating a table 70 | ---------------- 71 | Creating a table is simple, just create an instance of `Table`: 72 | ```python 73 | t = Table() 74 | ``` 75 | 76 | If you want, you can name the table at creation time, or any time later. 77 | ```python 78 | t = Table("customers") 79 | ``` 80 | 81 | or 82 | 83 | ```python 84 | t = Table() 85 | t("customers") 86 | ``` 87 | 88 | Table names are not necessary for queries or updates, as they would be in SQL. 89 | Table names can be useful in diagnosing problems, as they will be included in 90 | exception messages. Table joins also use the names of the source tables to 91 | create a helpful name for the resulting data table. 92 | 93 | Once you have created the Table, you can then use `insert` or `insert_many` to 94 | populate the table, or use one of the `import` methods (such as `csv_import`, 95 | `tsv_import`, or `json_import`) to load the table from a data string, external file, 96 | compressed file, Excel spreadsheet, or Internet URL. 97 | 98 | 99 | Inserting objects 100 | ----------------- 101 | From within your Python code, you can create objects and add them to the table using 102 | `insert()` and `insert_many()`. Any object can be inserted into a table, using: 103 | 104 | ```python 105 | t.insert(obj) 106 | t.insert_many(objlist) 107 | ``` 108 | 109 | Performance tip: Calling `insert_many()` with a list of objects will perform better than calling 110 | `insert()` in a loop. 111 | 112 | Importing data from CSV files 113 | ----------------------------- 114 | You can easily import a CSV file into a `Table` using `Table.csv_import()`: 115 | 116 | ```python 117 | t = Table().csv_import("my_data.csv") 118 | ``` 119 | 120 | or: 121 | 122 | ```python 123 | import littletable as lt 124 | t = lt.csv_import("my_data.csv") 125 | ``` 126 | 127 | In place of a local file name, you can specify an HTTP url (see 128 | [Importing from remote sources using HTTP](#importing-from-remote-sources-using-http) for more 129 | details): 130 | 131 | ```python 132 | url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/iris.csv" 133 | names = ["sepal-length", "sepal-width", "petal-length", "petal-width", "class"] 134 | iris_table = Table('iris').csv_import(url, fieldnames=names) 135 | ``` 136 | 137 | You can directly import CSV data as a string: 138 | ```python 139 | catalog_data = """\ 140 | sku,description,unitofmeas,unitprice 141 | BRDSD-001,Bird seed,LB,3 142 | BBS-001,Steel BB's,LB,5 143 | MGNT-001,Magnet,EA,8""" 144 | 145 | catalog = Table("catalog") 146 | catalog.create_index("sku", unique=True) 147 | catalog.csv_import(catalog_data, transforms={'unitprice': int}) 148 | ``` 149 | 150 | If you are working with a very large CSV file and just trying to see 151 | what the structure is, add `limit=100` to only read the first 100 rows. 152 | 153 | You can pre-screen data as it is read from the input file by passing 154 | a `filters={attr: filter_fn, ...}` argument. Each filter function is called 155 | on the newly-read object _before_ it is added to the table. `filter_fn` 156 | can be any function that takes a single argument of the type of the given 157 | attribute, and returns `True` or `False`. If `False`, the record does not get 158 | added to the table. 159 | 160 | ```python 161 | # import only the first 100 items that match the filter 162 | # (product_category == "Home and Garden") 163 | catalog.csv_import(catalog_data, 164 | filters={"product_category": Table.eq("Home and Garden")}, 165 | limit=100) 166 | ``` 167 | 168 | Since CSV files do not keep any type information, `littletable` will use the 169 | `SimpleNamespace` type for imported records. You can specify your own type by 170 | passing `row_class=MyType` to `csv_import`. The type must be initializable 171 | using the form `MyType(**attributes_dict)`. `namedtuples` and `SimpleNamespace` 172 | both support this form. 173 | 174 | Performance tip: For very large files, it is faster to load data using 175 | a `dataclass` or `namedtuple` than to use the default `SimpleNamespace` class. 176 | Get the fields using 177 | a 10-item import using `limit=10`, and then define the `namedtuple` using the 178 | fields from `table.info()["fields"]`. 179 | 180 | Files containing JSON-formatted records can be similarly imported using 181 | `Table.json_import()`, and tab-separated files can be imported using 182 | `Table.tsv_import()`. 183 | 184 | `littletable` can also read CSV, TSV, etc. content directly from a simple `.zip`, 185 | `.gz`, `.tar.gz`, or `.xz` archive. If the archive contains multiple files, it 186 | will try to read the contained file that matches the name of the archive, after 187 | dropping the compression extension string - otherwise it will raise a `ValueError`. 188 | (For `.zip` files, an optional `zippath` argument can be used to select a specific 189 | CSV, TSV, or JSON file by its path into the zip archive.) 190 | 191 | Calling `csv_import` on the same `Table` multiple times with different CSV files 192 | accumulates all the data into a single table. 193 | 194 | Note: if you find you cannot import `.xz` or `.lzma` files, getting the Python error 195 | `ModuleNotFoundError :_lzma`, you can remedy this by rebuilding Python after 196 | installing the `lzma-dev` library. On Ubuntu for example, this is done using: 197 | 198 | $ sudo apt-get install liblzma-dev 199 | 200 | Then rebuild and reinstall Python. 201 | 202 | 203 | Transforming data while importing 204 | --------------------------------- 205 | By default, all data imported from CSV (and related untyped data formats) is loaded 206 | as Python strings (`str` type). To convert data of other types, add the `transforms` argument, 207 | defining conversion functions to use on specific fields of the imported data. Conversion 208 | functions must take a single `str` argument, and return the converted value or raise an 209 | exception. 210 | 211 | Given input data of this form: 212 | 213 | ``` 214 | label,a,b,c,date_created 215 | A,1,2,3,2020/01/01 216 | B,4,5,6,2020/01/02 217 | C,7,8,9,2020/01/03 218 | ``` 219 | 220 | You can convert the numeric and date fields during the import using: 221 | 222 | ```python 223 | table = lt.csv_import( 224 | data, 225 | transforms={ 226 | "a": int, 227 | "b": int, 228 | "c": int, 229 | "date_created": lambda s: datetime.datetime.strptime(s, "%Y/%m/%d"), 230 | } 231 | ) 232 | ``` 233 | 234 | This can be a lot of work when an input file contains many numeric fields to be converted. 235 | To simplify this case, you can use a "*" wildcard value to define a conversion function 236 | for all fields that do not have other defined transforms. For the above case, you can write: 237 | 238 | ```python 239 | table = lt.csv_import( 240 | data, 241 | transforms={ 242 | "*": int, 243 | "date_created": lambda s: datetime.datetime.strptime(s, "%Y/%m/%d"), 244 | } 245 | ) 246 | ``` 247 | 248 | By default, fields whose values raise an exception during version remain unchanged (such as 249 | all the "label" values in the above example). You can specify an error default value to be 250 | used instead by passing a `(function, default_value)` tuple as the transform value. 251 | 252 | Aside from the Python builtin functions `int` and `float`, the `littletable.Table` class 253 | includes some useful transform staticmethods: 254 | - `convert_numeric` 255 | - `parse_date` 256 | - `parse_datetime` 257 | - `parse_timedelta` 258 | 259 | See the help text for these methods for more details. 260 | 261 | 262 | Import/export to Excel files (.xlsx) 263 | ------------------------------------ 264 | `littletable` can read and write local Excel spreadsheet files: 265 | 266 | ```python 267 | tbl = lt.Table().excel_import("data_table.xlsx") 268 | tbl.excel_export("new_table.xlsx") 269 | ``` 270 | 271 | Data values from Excel get converted to standard Python types where possible. 272 | A spreadsheet containing the following data: 273 | 274 | | name | value | type | 275 | |------|---------------------|---------| 276 | | a | 100 | int | 277 | | b | 3.14159 | float | 278 | | c | None | null | 279 | | d | 2021-12-25 00:00:00 | date | 280 | | e | Floyd | str | 281 | | f | | space | 282 | | g | 𝚃𝖞𝐩𝓮𝖤𝔯𝘳º𝗿 | str | 283 | | h | True | bool | 284 | | i | =TODAY() | formula | 285 | | j | 0 | None | 286 | | k | None | None | 287 | 288 | Can be imported and the data values will be automatically 289 | converted as shown below: 290 | 291 | ```python 292 | xl = lt.Table().excel_import("../test/data_types.xlsx") 293 | 294 | for row in xl: 295 | print(row.name, repr(row.value), type(row.value), row.type) 296 | 297 | 298 | a 100 int 299 | b 3.14159 float 300 | c None null 301 | d datetime.datetime(2021, 12, 25, 0, 0) date 302 | e 'Floyd' str 303 | f ' ' space 304 | g '𝚃𝖞𝐩𝓮𝖤𝔯𝘳º𝗿' str 305 | h True bool 306 | i '=TODAY()' formula 307 | j 0 None 308 | k None None 309 | ``` 310 | 311 | `littletable` uses `openpyxl` package to read and write Excel files. When writing 312 | using `littletable.Table.excel_export`, `openpyxl` is optimized when an XML 313 | serialization package such as `defusedxml` or `lxml` is installed. `defusedxml` is 314 | preferred, as it guards against some known XML data attacks. 315 | 316 | 317 | Importing from remote sources using HTTP 318 | ---------------------------------------- 319 | When importing from an HTTP or HTTPS source, you can optionally specify the following 320 | named arguments: 321 | 322 | | Argument | Type | Description | 323 | |----------|----------------|-------------------------------------------------------------------------------------------------------------------| 324 | | headers | dict | Headers to be passed as part of the HTTP request | 325 | | data | bytes | Data to pass as the body of the request (will use the HTTP POST method in place of the default GET) | 326 | | username | str | Username to pass using Basic Authentication; will generate `Authorization` header | 327 | | password | str | Password for Basic Authentication, default = "" | 328 | | cafile | str | SSL certificate file, default = None | 329 | | capath | str | Directory of SSL certificate files, default = None | 330 | | cadata | str or bytes | Either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates | 331 | | context | ssl.SSLContext | SSLContext instance, default = None | 332 | | timeout | int | Timeout in seconds, default = littletable.DEFAULT_HTTP_TIMEOUT (initialized to 60) | 333 | 334 | _(Note: when passing username and password credentials, HTTPS URLs are strongly encouraged, to prevent 335 | exposure of credentials in unencrypted requests. `littletable` will emit a warning when importing with authentication 336 | using an HTTP URL.)_ 337 | 338 | Some sites will use the "User-Agent" HTTP header to filter out unrestricted automated access to their site. You can add this header to your 339 | import statement using code as in the following: 340 | 341 | user_agent_string = ( 342 | "Mozilla/5.0" 343 | " (Windows NT 10.0; Win64; x64)" 344 | " AppleWebKit/537.36" 345 | " (KHTML, like Gecko)" 346 | " Chrome/128.0.0.0" 347 | " Safari/537.36" 348 | " Edg/128.0.0.0" 349 | ) 350 | table = littletable.csv_import(url, headers={"User-Agent": user_agent_string}) 351 | 352 | Tabular output 353 | -------------- 354 | To produce a nice tabular output for a table, you can use the embedded support for 355 | the `rich` module, `as_html()` in Jupyter Notebook, or the `tabulate` module: 356 | 357 | - Using `table.present()` (implemented using `rich`; `present()` accepts `rich` Table 358 | keyword args): 359 | 360 | ```python 361 | table(title_str).present(fields=["col1", "col2", "col3"]) 362 | ``` 363 | 364 | or 365 | 366 | ```python 367 | # use select() to limit the columns to be shown 368 | table.select("col1 col2 col3")(title_str).present(caption="caption text") 369 | ``` 370 | 371 | `table.present()` also accepts a `width` argument, to override any terminal default width setting: 372 | 373 | ```python 374 | table(title_str).present(width=400) 375 | ``` 376 | 377 | - Using `Jupyter Notebook`: 378 | 379 | ```python 380 | from IPython.display import HTML, display 381 | display(HTML(table.as_html(table_properties={"border": 1, "cellpadding": 5}))) 382 | ``` 383 | 384 | - Using `tabulate`: 385 | ```python 386 | # use map(vars, table) to get each table record as a dict, then pass to tabulate with 387 | # headers="keys" to auto-define headers 388 | print(tabulate(map(vars, table), headers="keys")) 389 | ``` 390 | 391 | - Output as Markdown 392 | 393 | ```python 394 | print(table.as_markdown()) 395 | ``` 396 | 397 | You can display groups in your tables by specifying a particular field on which to group. 398 | Pass the `groupby` argument to `present()`, `as_html()` or `as_markdown()` with the name of the 399 | field or fields, and consecutive duplicate values for that field will be suppressed. 400 | 401 | 402 | types.SimpleNamespace 403 | --------------------- 404 | If your program does not have a type for inserting into your table, you can use 405 | `types.SimpleNamespace`: 406 | 407 | ```python 408 | from types import SimpleNamespace 409 | bob = {"name": "Bob", "age": 19} 410 | t.insert(SimpleNamespace(**bob)) 411 | ``` 412 | 413 | Or just use `dict`s directly (which `littletable` will convert to `SimpleNamespace`s) 414 | 415 | ```python 416 | bob = {"name": "Bob", "age": 19} 417 | t.insert(bob) 418 | ``` 419 | 420 | Removing objects 421 | ---------------- 422 | Objects can be removed individually or by passing a list (or `Table`) of 423 | objects: 424 | 425 | ```python 426 | t.remove(obj) 427 | t.remove_many(objlist) 428 | t.remove_many(t.where(a=100)) 429 | ``` 430 | 431 | They can also be removed by numeric index or slice using the Python `del` statement, just 432 | like removing from a list: 433 | 434 | ```python 435 | del t[0] 436 | del t[-1] 437 | del t[3:10] 438 | ``` 439 | 440 | Finally, items can be removed using `Table.pop()`, which like `list.pop` defaults to removing 441 | the last item in the table, and returns the removed item: 442 | 443 | ```python 444 | obj = t.pop(12) 445 | obj = t.pop() 446 | ``` 447 | 448 | Adding new fields to existing rows 449 | ---------------------------------- 450 | If the underlying storage types for the rows in the `Table` permit the addition of 451 | new attributes and/or modification of existing attributes, you can do so using 452 | `Table.compute_field`, passing in a callable that `compute_field` will use to 453 | compute the value for this field for each record. 454 | 455 | You can see an example of this in `examples/explore_unicode.py`, which builds a table 456 | from the contents of the official Unicode CSV file: 457 | 458 | ```python 459 | # import the Unicode definitions, including the name and hex character value for each code point 460 | unicode = lt.csv_import(unicode_csv_file, delimiter=';') 461 | 462 | # add a field containing the integer code point value, computed from the given code_value_hex field 463 | unicode.compute_field("code_value", lambda r: int(r.code_value_hex, 16)) 464 | 465 | # add a field containing the actual Unicode character for each row 466 | unicode.compute_field("character", lambda r: chr(r.code_value)) 467 | 468 | # add a boolean field indicating whether the character can be used as an identifier character 469 | unicode.compute_field("is_identifier", lambda r: r.character.isidentifier()) 470 | ``` 471 | 472 | You can also overwrite existing fields using `compute_field`. 473 | 474 | `compute_field` also accepts a `default` argument, to be used if the given callable raises 475 | an exception for a particular row; if no default is specified, `None` is used. 476 | 477 | (`compute_field` was formerly named `add_field`, which is still maintained for compatibility.) 478 | 479 | 480 | Indexing attributes 481 | ------------------- 482 | Use `create_index` to add an index to a `Table`. Indexes can be unique or 483 | non-unique. If the table is not empty and the index to be created is 484 | `unique=True`, the uniqueness of the index attribute across the existing 485 | records is verified before creating the index, raising `KeyError` and 486 | listing the duplicated value. 487 | 488 | If a unique index is created, then retrieving using that index will 489 | return the single matching object, or raise `KeyError`. 490 | 491 | If a non-unique index is created, a `Table` is returned of all the matching 492 | objects. If no objects match, an empty `Table` is returned. 493 | 494 | ```python 495 | employees.create_index('employee_id', unique=True) 496 | employees.create_index('zipcode') 497 | 498 | # unique indexes return a single object 499 | print(employees.by.employee_id["D1729"].name) 500 | 501 | # non unique indexes return a new Table 502 | for emp in employees.by.zipcode["12345"]: 503 | print(e.name) 504 | ``` 505 | 506 | 507 | Querying with indexed attributes 508 | -------------------------------- 509 | 510 | If accessing a table using a unique index, giving a key value will 511 | return the single matching record, or raise `KeyError`. 512 | 513 | ```python 514 | employees.by.employee_id['00086'] 515 | employees.by.employee_id['invalid_id'] 516 | # raises KeyError: "no such value 'invalid_id' in index 'employee_id'" 517 | ``` 518 | 519 | If accessing a table using a non-unique index, will return a new `Table` 520 | containing all matching records. If there are no matching records, the 521 | returned table will be empty. 522 | 523 | ```python 524 | employees.by.state['CA'] 525 | employees.by.dept['Sales'] 526 | employees.by.dept['Salex'] # no such department 527 | # returns empty table 528 | ``` 529 | 530 | You can also use the get() method, similar to Python's `dict.get()`: 531 | ```python 532 | # using the unique index on "employee_id" 533 | employees.by.employee_id.get('00086') # returns the matching record 534 | employees.by.employee_id.get('invalid_id') # returns None 535 | 536 | # using the non-unique index on "dept" 537 | employees.by.dept['Sales'] # returns a Table of matching records 538 | employees.by.dept['Salex'] # returns None 539 | ``` 540 | 541 | Querying for exact matching attribute values 542 | -------------------------------------------- 543 | Calling `Table.where()` with named attributes will return a Table of 544 | all records matching all the arguments: 545 | 546 | ```python 547 | employees.where(zipcode="12345", title="Manager") 548 | 549 | # Python keywords have to be passed in an unpacked dict. 550 | student.where(**{"class": "Algebra"}) 551 | ``` 552 | 553 | It is not necessary for the attributes to be indexed to use `Table.where()`. 554 | 555 | 556 | Querying for attribute value ranges 557 | ----------------------------------- 558 | `Table.where()` supports performing queries on one or more exact 559 | matches against entries in the table: 560 | 561 | ```python 562 | employees.where(dept="Engineering") 563 | ``` 564 | 565 | `Table.where()` will also accept a callable that takes a record and 566 | returns a bool to indicate if the record is a match: 567 | 568 | ```python 569 | employees.where(lambda emp: emp.salary > 50000) 570 | ``` 571 | 572 | `littletable` includes _comparators_ to make range-checking easier to 573 | write. The following table lists the comparators, plus examples of their 574 | usage: 575 | 576 | | Comparator | Example | Comparison performed | 577 | |---------------|:--------------------------------|:-------------------------------| 578 | | `lt` | `attr=Table.lt(100)` | `attr < 100` | 579 | | `le` | `attr=Table.le(100)` | `attr <= 100` | 580 | | `gt` | `attr=Table.gt(100)` | `attr > 100` | 581 | | `ge` | `attr=Table.ge(100)` | `attr >= 100` | 582 | | `eq` | `attr=Table.eq(100)` | `attr == 100` | 583 | | `ne` | `attr=Table.ne(100)` | `attr != 100` | 584 | | `is_none` | `attr=Table.is_none())` | `attr is None` | 585 | | `is_not_none` | `attr=Table.is_not_none())` | `attr is not None` | 586 | | `is_null` | `attr=Table.is_null())` | `attr is None, "", or omitted` | 587 | | `is_not_null` | `attr=Table.is_not_null())` | `attr is not None or ""` | 588 | | `startswith` | `attr=Table.startswith("ABC")` | `attr.startswith("ABC")` | 589 | | `endswith` | `attr=Table.endswith("XYZ")` | `attr.endswith("XYZ")` | 590 | | `between` | `attr=Table.between(100, 200)` | `100 < attr < 200` | 591 | | `within` | `attr=Table.within(100, 200)` | `100 <= attr <= 200` | 592 | | `in_range` | `attr=Table.in_range(100, 200)` | `100 <= attr < 200` | 593 | | `is_in` | `attr=Table.is_in((1, 2, 3))` | `attr in (1,2,3)` | 594 | | `not_in` | `attr=Table.not_in((1, 2, 3))` | `attr not in (1,2,3)` | 595 | 596 | More examples of comparators in actual Python code: 597 | 598 | ```python 599 | employees.where(salary=Table.gt(50000)) 600 | employees.where(dept=Table.is_in(["Sales", "Marketing"])) 601 | 602 | jan_01 = date(2000, 1, 1) 603 | mar_31 = date(2000, 3, 31) 604 | apr_01 = date(2000, 4, 1) 605 | 606 | first_qtr_sales = sales.where(date=Table.within(jan_01, mar_31)) 607 | first_qtr_sales = sales.where(date=Table.in_range(jan_01, apr_01)) 608 | 609 | # get customers whose address includes an apartment number 610 | has_apt = customers.where(address_apt_no=Table.is_not_null()) 611 | 612 | # get employees whose first name starts with "X" 613 | x_names = employees.where(name=Table.startswith("X")) 614 | 615 | # get log records that match a regex (any word starts with 616 | # "warn" in the log description) 617 | import re 618 | warnings = log.where(description = re.compile(r"\bwarn", flags=re.I).search) 619 | ``` 620 | 621 | Comparators can also be used as filter functions for import methods. 622 | 623 | [Added in version 2.0.6] 624 | 625 | You can write your own comparator functions also. Define a function that 626 | takes a single value as would be stored in an attribute, and call 627 | `Table.where()` using `attribute=function`: 628 | 629 | ```python 630 | # For a table with an int field 'a', select all the records where 631 | # 'a' is odd. 632 | 633 | def is_odd(x): 634 | return bool(x % 2) 635 | 636 | # formerly 637 | tbl.where(lambda rec: is_odd(rec.a)) 638 | 639 | # new simplified form 640 | tbl.where(a=is_odd) 641 | 642 | # a comparator using regex 643 | import re 644 | # products whose name starts with "cotton" or "linen" 645 | tbl.where(product_name=re.compile(r"(cotton|linen)\b", re.I).match) 646 | # products whose name contains the word "cotton" or "linen" 647 | tbl.where(product_name=re.compile(r"\b(cotton|linen)\b", re.I).search) 648 | ``` 649 | 650 | Range querying on indexed attributes using slice notation 651 | --------------------------------------------------------- 652 | 653 | [Added in version 2.0.7] 654 | 655 | For indexed fields, range queries can also be done using slice notation. 656 | Compare these examples with operations shown [above](#querying-for-attribute-value-ranges) 657 | (given in comments): 658 | 659 | ```python 660 | # employees.where(salary=Table.ge(50000)) 661 | employees.create_index("salary") 662 | employees.by.salary[50000:] 663 | ``` 664 | 665 | Unlike Python list slices, `Table` index slices can use non-integer data 666 | types (as long as they support `>=` and `<` comparison operations): 667 | 668 | ```python 669 | jan_01 = datetime.date(2000, 1, 1) 670 | apr_01 = datetime.date(2000, 4, 1) 671 | 672 | # first_qtr_sales = sales.where(date=Table.in_range(jan_01, apr_01)) 673 | sales.create_index("date") 674 | first_qtr_sales = sales.by.date[jan_01: apr_01] 675 | ``` 676 | 677 | Note that slices with a step field (as in `[start : stop : step]`) are not supported. 678 | 679 | 680 | Splitting a table using a criteria function 681 | ------------------------------------------- 682 | You can divide a `littletable.Table` into 2 new tables using 683 | `Table.splitby`. `Table.splitby` takes a predicate function that takes 684 | a table record and returns True or False, and returns two tables: 685 | a table with all the rows that returned False and a table with all the 686 | rows that returned True. Will also accept a string indicating a particular 687 | field name, and uses `bool(getattr(rec, field_name))` for the predicate 688 | function. 689 | 690 | ```python 691 | # split on records based on even/odd of a value attribute 692 | is_odd = lambda x: bool(x % 2) 693 | evens, odds = tbl.splitby(lambda rec: is_odd(rec.value)) 694 | 695 | # split on an integer field: 0 will be treated as False, non-zero as True 696 | has_no_cars, has_cars = tbl.splitby("number_of_cars_owned") 697 | 698 | # split on a field that may be None or "" 699 | nulls, not_nulls = tbl.splitby("optional_data_field") 700 | ``` 701 | 702 | A shorthand for specifying a predicate function can be used if the predicate 703 | is solely a test for a specific value for one or more attributes: 704 | 705 | ```python 706 | qa_data, production_assembly_data = tbl.splitby( 707 | lambda rec: rec.env == "prod" and rec.dept == "assembly" 708 | ) 709 | # can be written as 710 | qa_data, production_data = tbl.splitby(env="prod", dept="assembly") 711 | ``` 712 | 713 | An optional `errors` argument allows you to define what action 714 | to take if an exception occurs while evaluating the predicate function. 715 | Valid values for `errors` are: 716 | 717 | - True : return exceptions as True 718 | - False: return exceptions as False 719 | - 'discard': do not return table rows that raise exceptions 720 | - 'return': return a third table containing rows that raise exceptions 721 | - 'raise': raise the exception 722 | 723 | `errors` can also be given as a dict mapping Exception types to one of 724 | these 4 values. 725 | 726 | The default value for `errors` (if omitted, or if an exception is raised 727 | that is not listed in an `errors` dict), is to discard the row. 728 | 729 | 730 | Returning grouped tables from a table 731 | ------------------------------------- 732 | `Table.groupby` is very similar to `itertools.groupby`, yielding a `(key, Table)` tuple for 733 | each grouped list of rows. Includes an optional `sort` argument to sort the table before grouping, 734 | using the same key attributes or function used for grouping. 735 | 736 | 737 | Splitting a table into smaller batches 738 | -------------------------------------- 739 | You can break a `Table` into smaller-sized tables (such as might be 740 | needed for paginated reporting), using `Table.batched`, similar to 741 | `itertools.batched` added in Python 3.13. Returns a generator that 742 | yields tables sliced into n-sized batches: 743 | 744 | for mini_table in tbl.batched(10): 745 | ... work with table containing only 10 entries ... 746 | 747 | 748 | Full-text search on text attributes 749 | ----------------------------------- 750 | `littletable` can perform a rudimentary version of full-text search against 751 | attributes composed of multiple-word contents (such as item descriptions, 752 | comments, etc.). To perform a full-text search, a search index must first be 753 | created, using `Table.create_search_index()`, naming the attribute to be 754 | indexed, and optionally any stop words that should be ignored. 755 | 756 | Afterward, queries can be run using `table.search.(query)`, 757 | where `attribute` is the attribute that was indexed, and `query` is 758 | a list or space-delimited string of search terms. Search terms may be 759 | prefixed by '++' or '--' to indicate required or prohibited terms, or 760 | '+' or '-' for preferred or non-preferred terms. The search function 761 | uses these prefixes to compute a matching score, and the matching records 762 | are returned in descending score order, along with their scores, and optionally 763 | each record's parsed keywords. 764 | 765 | In addition to the query, you may also specify a limit; whether to include 766 | each entry's indexed search words; and whether to return the results as a new 767 | table or as a list of (record, search_score) tuples. 768 | 769 | Example: 770 | 771 | ```python 772 | recipe_data = textwrap.dedent("""\ 773 | title,ingredients 774 | Tuna casserole,tuna noodles cream of mushroom soup 775 | Hawaiian pizza,pizza dough pineapple ham tomato sauce 776 | BLT,bread bacon lettuce tomato mayonnaise 777 | Bacon cheeseburger,ground beef bun lettuce ketchup mustard pickle cheese bacon 778 | """) 779 | recipes = lt.Table().csv_import(recipe_data) 780 | 781 | recipes.create_search_index("ingredients") 782 | matches = recipes.search.ingredients("+bacon tomato --pineapple", as_table=True) 783 | matches.present() 784 | ``` 785 | 786 | Will display: 787 | 788 | ``` 789 | +bacon tomato --pineapple 790 | 791 | Title Ingredients Ingredients Search Score 792 | ─────────────────────────────────────────────────────────────────────────── 793 | BLT bread bacon lettuce 1100 794 | tomato mayonnaise 795 | Bacon cheeseburger ground beef bun lettuce 1000 796 | ketchup mustard pickle 797 | cheese bacon 798 | 799 | ``` 800 | 801 | In some cases, the terms to search for are spread across multiple attributes. 802 | For these cases, create_search_index accepts an optional `using` argument, 803 | containing a list of string attributes to concatenate into a single searchable 804 | attribute: 805 | 806 | ```python 807 | recipes.create_search_index("recipe_terms", using="ingredients title") 808 | matches = recipes.search.recipe_terms("casserole Hawaiian", as_table=True) 809 | matches.present(fields="title ingredients".split()) 810 | ``` 811 | 812 | Will display: 813 | ``` 814 | Title Ingredients 815 | ───────────────────────────────────────────────────────── 816 | Tuna casserole tuna noodles cream of mushroom soup 817 | Hawaiian pizza pizza dough pineapple ham tomato sauce 818 | ``` 819 | 820 | Search indexes will become invalid if records are added or removed from the table 821 | after the index has been created. If they are not rebuilt, subsequent searches 822 | will raise the `SearchIndexInconsistentError` exception. 823 | 824 | 825 | Basic statistics on Tables of numeric values 826 | --------------------------------------------- 827 | `Table.stats()` will perform basic mean, variance, and standard deviation 828 | calculations by attribute on records in a table. The results are returned 829 | in a new `Table` that can be keyed by attribute (with "mean", "variance", etc. 830 | attributes), or by statistic (keyed by "mean", etc., with attributes matching 831 | those in the source `Table`). Non-numeric values are implicitly omitted from 832 | the statistics calculations. 833 | 834 | ```python 835 | import littletable as lt 836 | 837 | t1 = lt.Table() 838 | t1.csv_import("""\ 839 | a,b,c 840 | 100,101,102 841 | 110,220,99 842 | 108,130,109""", transforms=dict(a=int, b=int, c=int)) 843 | 844 | t1_stats = t1.stats() 845 | t1_stats.present() 846 | print(t1_stats.by.name["a"].mean) 847 | ``` 848 | 849 | Prints: 850 | 851 | ``` 852 | Name Mean Median Min Max Variance Std Dev Count Missing 853 | ───────────────────────────────────────────────────────────────────────── 854 | a 106.0 108 100 110 28 5.292 3 0 855 | b 150.3 130 101 220 3850.0 62.05 3 0 856 | c 103.3 102 99 109 26.33 5.132 3 0 857 | 858 | 106.0 859 | ``` 860 | 861 | This same data computed by stat instead of by field name. 862 | 863 | ```python 864 | t1_stats = t1.stats(by_field=False) 865 | t1_stats.present(box=lt.box.ASCII) # uses ASCII table borders 866 | print(t1_stats.by.stat["mean"].a) 867 | ``` 868 | 869 | Prints: 870 | 871 | ``` 872 | +-----------------------------------+ 873 | | Stat | A | B | C | 874 | |----------+-------+--------+-------| 875 | | mean | 106.0 | 150.3 | 103.3 | 876 | | median | 108 | 130 | 102 | 877 | | min | 100 | 101 | 99 | 878 | | max | 110 | 220 | 109 | 879 | | variance | 28 | 3850.0 | 26.33 | 880 | | std_dev | 5.292 | 62.05 | 5.132 | 881 | | count | 3 | 3 | 3 | 882 | | missing | 0 | 0 | 0 | 883 | +-----------------------------------+ 884 | 106.0 885 | ``` 886 | 887 | 888 | Importing data from fixed-width text files 889 | ---------------------------------------------- 890 | Some files contain fixed-width columns, you can use the `FixedWidthReader` 891 | class to import these into `littletable` `Table`s. 892 | 893 | For data in this data file (not including the leading rows showing 894 | column numbers): 895 | 896 | 1 2 3 4 5 6 897 | 0123456789012345678901234567890123456789012345678901234567890123456789 898 | --- 899 | 0010GEORGE JETSON 12345 SPACESHIP ST HOUSTON TX 4.9 900 | 0020WILE E COYOTE 312 ACME BLVD TUCSON AZ 7.3 901 | 0030FRED FLINTSTONE 246 GRANITE LANE BEDROCK CA 2.6 902 | 0040JONNY QUEST 31416 SCIENCE AVE PALO ALTO CA 8.1 903 | 904 | Define the columns to import as: 905 | 906 | ```python 907 | columns = [ 908 | ("id_no", 0, ), 909 | ("name", 4, ), 910 | ("address", 21, ), 911 | ("city", 42, ), 912 | ("state", 56, 58, ), 913 | ("tech_skill_score", 59, None, float), 914 | ] 915 | ``` 916 | 917 | And use a `FixedWidthReader` to read the file and pass a list of 918 | dicts to a `Table.insert_many`: 919 | 920 | ```python 921 | characters = lt.Table() 922 | reader = lt.FixedWidthReader(columns, "cartoon_characters.txt") 923 | characters.insert_many(reader) 924 | ``` 925 | 926 | For each column, define: 927 | - the attribute name 928 | - the starting column 929 | - (optional) the ending column (the start of the next column is 930 | the default) 931 | - (optional) a function to transform the input string (in this 932 | example, 'tech_skill_score' gets converted to a float); if no 933 | function is specified, str.strip() is used 934 | 935 | 936 | Joining tables 937 | -------------- 938 | Joining tables is one of the basic functions of relational databases. 939 | To join two tables, you must specify: 940 | - the left source table and join attribute 941 | - the right source table and join attribute 942 | - whether the join should be performed as an inner join, left outer 943 | join, right outer join, or full outer join (default is an inner join) 944 | - optionally, a list of the attributes to include in the resulting join 945 | data (returned in `littletable` as a new `Table`) 946 | 947 | `littletable` provides two different coding styles for joining tables. 948 | The first uses conventional object notation, with the `table.join()` 949 | method: 950 | 951 | ```python 952 | customers.join(orders, custid="custid") 953 | ``` 954 | 955 | creates an inner join between the table of customers and the table of 956 | their respective orders, joining on both tables' `custid` attributes. 957 | 958 | More than 2 tables can be joined in succession, since the result of a 959 | join is itself a `Table`: 960 | 961 | ```python 962 | customers.join(orders, custid="custid").join(orderitems, orderid="orderid") 963 | ``` 964 | 965 | In this case a third table has been added, to include the actual items 966 | that comprise each customer's order. The `orderitems` are associated with 967 | each order by `orderid`, and so the additional join uses that field to 968 | associate the joined customer-orders table with the `orderitems` table. 969 | 970 | The second coding style takes advantage of Python's support for customizing 971 | the behavior of arithmetic operators. A natural operator for joining two 972 | tables would be the '+' operator. To complete the join specification, we 973 | need not only the tables to be joined (the left and right terms of the '+' 974 | operation), but also the attributes to use to know which objects of each 975 | table to join together. To support this, tables have the join_on() method, 976 | which return a `JoinTerm` object: 977 | 978 | ```python 979 | customers.join_on("custid") + orders.join_on("custid") 980 | ``` 981 | 982 | This returns a join expression, which when called, performs the join and 983 | returns the data as a new `Table`: 984 | 985 | ```python 986 | customerorders = (customers.join_on("custid") + orders.join_on("custid"))() 987 | ``` 988 | 989 | JoinTerms can be added to tables directly when the join table and the added 990 | table are to join using the same attribute name. The 3-table join above 991 | can be written as: 992 | 993 | ```python 994 | customerorderitems = ((customers.join_on("custid") 995 | + orders 996 | + orderitems.join_on("orderid"))()) 997 | ``` 998 | 999 | A classic example of performing an outer join is, given a table of students 1000 | and a table of student->course registrations, find the students who 1001 | are not registered for any courses. The solution is to perform an outer 1002 | join, and select those students where their course registration is NULL. 1003 | 1004 | Here is how that looks with littletable: 1005 | 1006 | ```python 1007 | # define student and registration data 1008 | students = lt.Table().csv_import("""\ 1009 | student_id,name 1010 | 0001,Alice 1011 | 0002,Bob 1012 | 0003,Charlie 1013 | 0004,Dave 1014 | 0005,Enid 1015 | """) 1016 | 1017 | registrations = lt.Table().csv_import("""\ 1018 | student_id,course 1019 | 0001,PSYCH101 1020 | 0001,CALC1 1021 | 0003,BIO200 1022 | 0005,CHEM101 1023 | """) 1024 | 1025 | # perform outer join and show results: 1026 | non_reg = students.outer_join(lt.Table.RIGHT_OUTER_JOIN, 1027 | registrations, 1028 | student_id="student_id").where(course=None) 1029 | non_reg.select("student_id name").present() 1030 | print(list(non_reg.all.name)) 1031 | ``` 1032 | 1033 | Displays: 1034 | 1035 | ``` 1036 | Student Id Name 1037 | ─────────────────── 1038 | 0002 Bob 1039 | 0004 Dave 1040 | 1041 | ['Bob', 'Dave'] 1042 | ``` 1043 | 1044 | 1045 | Pivoting a table 1046 | ---------------- 1047 | Pivoting is a useful function for extracting overall distributions of values 1048 | within a table. Tables can be pivoted on 1, 2, or 3 attributes. The pivot 1049 | tallies up the number of objects in a given table with each of the different 1050 | key values. A single attribute pivot gives the same results as a histogram - 1051 | each key for the attribute is given, along with the count of objects having 1052 | that key value. (Of course, pivots are most interesting for attributes 1053 | with non-unique indexes.) 1054 | 1055 | Pivoting on 2 attributes extends the concept, getting the range of key values 1056 | for each attribute, and then tallying the number of objects containing each 1057 | possible pair of key values. The results can be reported as a two-dimensional 1058 | table, with the primary attribute keys down the leftmost column, and the 1059 | secondary attribute keys as headers across the columns. Subtotals will 1060 | be reported at the far right column and at the bottom of each column. 1061 | 1062 | 1063 | Optional dependencies 1064 | --------------------- 1065 | The base `littletable` code has no dependencies outside of the Python stdlib. However, some operations 1066 | require additional package installs: 1067 | 1068 | | operation | additional install required | 1069 | |-----------------------------|--------------------------------------------------------------------| 1070 | | `Table.present` | `rich` | 1071 | | `Table.excel_import/export` | `openpyxl` (plus `defusedxml` or `lxml`, `defusedxml` recommended) | 1072 | | `Table.as_dataframe` | `pandas` | 1073 | 1074 | 1075 | littletable and pandas 1076 | ---------------------- 1077 | The `pandas` package is a mature and powerful data analysis module for Python, 1078 | with extensive analytical, statistical, and query features. It supports a 1079 | vectorized approach to many common data operations, so that operations on an 1080 | entire column of values can be done very quickly. 1081 | 1082 | However, the `pandas` package is rather heavyweight, and for simple table 1083 | operations (such as reading and accessing values in a CSV file), it may be 1084 | overpowered or overcomplicated to install and learn. 1085 | 1086 | `littletable` is lightweight in comparison, and, as a pure Python module, is 1087 | not optimized for vector operations on its "columns". As a single module 1088 | Python file, its installation and distribution can be very easy, so 1089 | well suited to small data projects. Its design philosophy is to make simple 1090 | tasks (such as CSV import and tabular display) simple, make some difficult 1091 | tasks (such as pivot, join, and full text search) possible, but leave the 1092 | really difficult tasks to other packages, such as `pandas`. 1093 | 1094 | `littletable` is useful even with tables of up to 1 or 2 million rows (especially 1095 | with defined indexes); `pandas` can handle much larger datasets, and so is 1096 | more suited to large Data Science projects. 1097 | 1098 | Use `pandas` if: 1099 | - your data set is large 1100 | - your problem involves extensive vector operations on entire columns 1101 | of data in your table 1102 | - your problem is simple (CSV import), but you already have installed 1103 | `pandas` and are familiar with its usage 1104 | 1105 | Consider `littletable` if: 1106 | - you do not already have `pandas` installed, or are unfamiliar with using 1107 | DataFrames 1108 | - your dataset is no more than 1-2 million rows 1109 | - your data needs are simple CSV import/export, filtering, sorting, 1110 | join/pivot, text search, and presentation 1111 | 1112 | 1113 | littletable and SQLite 1114 | ---------------------- 1115 | `littletable` and `SQLite` have many similarities in intent; lightweight, 1116 | easy to deploy and distribute. But `SQLite` requires some of the same 1117 | schema design steps and SQL language learning of any relational database. 1118 | Mapping from Python program objects to database tables often requires use 1119 | of an ORM like `SQLAlchemy`. `littletable` allows you to store your Python 1120 | objects directly in a Table, and query against it using similar ORM-style 1121 | methods. 1122 | 1123 | `littletable` might even be useful as a prototyping tool, to work through 1124 | some data modeling and schema design concepts before making a commitment 1125 | to a specific schema of tables and indexes. 1126 | 1127 | 1128 | Some simple littletable recipes 1129 | ------------------------------- 1130 | 1131 | - Find objects with NULL attribute values (an object's attribute is considered 1132 | NULL if the object does not have that attribute, or if its value is None or ""): 1133 | 1134 | ```python 1135 | table.where(keyattr=Table.is_null()) 1136 | ``` 1137 | 1138 | 1139 | - Histogram of values of a particular attribute: 1140 | 1141 | ```python 1142 | # returns a table 1143 | table.pivot(attribute).summary_counts() 1144 | ``` 1145 | or 1146 | ```python 1147 | # prints the values to stdout in tabular form 1148 | table.pivot(attribute).dump_counts() 1149 | ``` 1150 | 1151 | 1152 | - Get a list of all key values for an indexed attribute: 1153 | 1154 | ```python 1155 | customers.by.zipcode.keys() 1156 | ``` 1157 | 1158 | 1159 | - Get a list of all values for any attribute: 1160 | 1161 | ```python 1162 | list(customers.all.first_name) 1163 | 1164 | # or get just the unique values 1165 | list(customers.all.first_name.unique) 1166 | 1167 | # get a tally of all the first names 1168 | from collections import Counter 1169 | tally = Counter(customers.all.first_name) 1170 | ``` 1171 | 1172 | 1173 | - Get a count of entries for each key value: 1174 | 1175 | ```python 1176 | customers.pivot("zipcode").dump_counts() 1177 | ``` 1178 | 1179 | - Sort table by attribute x 1180 | 1181 | ```python 1182 | employees.orderby("salary") 1183 | 1184 | # sort in descending order 1185 | employees.orderby("salary desc") 1186 | ``` 1187 | 1188 | - Sorted table by primary attribute x, secondary attribute y 1189 | 1190 | ```python 1191 | sales_employees = employees.where(dept="Sales").orderby("salary,commission") 1192 | ``` 1193 | 1194 | or 1195 | 1196 | ```python 1197 | employees.create_index("dept") 1198 | sales_employees = employees.by.dept["Sales"].orderby("salary,commission") 1199 | ``` 1200 | 1201 | - Get top 5 objects in table by value of attribute x, and add ranking field 1202 | ```python 1203 | # top 5 sales employees 1204 | employees.where(dept="Sales").orderby("sales desc")[:5].rank() 1205 | ``` 1206 | 1207 | - Find all employees whose first name starts with "X" 1208 | 1209 | ```python 1210 | employees.where(first_name=Table.startswith("X")) 1211 | ``` 1212 | 1213 | (or using sliced indexing): 1214 | 1215 | ```python 1216 | employees.create_index("first_name") 1217 | employees.by.first_name["X": "Y"] # same as `"X" <= first_name < "Y"` 1218 | ``` 1219 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "littletable" 7 | version = "3.0.3" 8 | description = "Python in-memory ORM database" 9 | readme = { file = "README.md", content-type = "text/markdown" } 10 | requires-python = ">=3.9" 11 | license = "MIT" 12 | authors = [ 13 | { name = "Paul McGuire", email = "ptmcg@austin.rr.com" } 14 | ] 15 | maintainers = [ 16 | { name = "Paul McGuire", email = "ptmcg@austin.rr.com" } 17 | ] 18 | urls = {Homepage = "https://github.com/ptmcg/littletable/", Download = "https://pypi.org/project/littletable/"} 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Intended Audience :: Developers", 22 | "Intended Audience :: Information Technology", 23 | "Intended Audience :: Science/Research", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Programming Language :: Python :: 3.14", 34 | "Topic :: Database" 35 | ] 36 | 37 | [tool.setuptools] 38 | py-modules = ["littletable"] 39 | -------------------------------------------------------------------------------- /requirements.dev: -------------------------------------------------------------------------------- 1 | attrs 2 | rich 3 | openpyxl 4 | traits 5 | traits-stubs 6 | traitlets 7 | pydantic 8 | pytest 9 | pyparsing 10 | tox 11 | bandit 12 | defusedxml 13 | -------------------------------------------------------------------------------- /test/abc.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | 0,0,0 3 | 0,0,1 4 | 0,0,2 5 | 0,1,0 6 | 0,1,1 7 | 0,1,2 8 | 0,2,0 9 | 0,2,1 10 | 0,2,2 11 | 1,0,0 12 | 1,0,1 13 | 1,0,2 14 | 1,1,0 15 | 1,1,1 16 | 1,1,2 17 | 1,2,0 18 | 1,2,1 19 | 1,2,2 20 | 2,0,0 21 | 2,0,1 22 | 2,0,2 23 | 2,1,0 24 | 2,1,1 25 | 2,1,2 26 | 2,2,0 27 | 2,2,1 28 | 2,2,2 29 | -------------------------------------------------------------------------------- /test/abc.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.csv.gz -------------------------------------------------------------------------------- /test/abc.csv.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.csv.tar.gz -------------------------------------------------------------------------------- /test/abc.csv.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.csv.xz -------------------------------------------------------------------------------- /test/abc.csv.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.csv.zip -------------------------------------------------------------------------------- /test/abc.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.json.gz -------------------------------------------------------------------------------- /test/abc.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.tar.gz -------------------------------------------------------------------------------- /test/abc.tsv: -------------------------------------------------------------------------------- 1 | a b c 2 | 0 0 0 3 | 0 0 1 4 | 0 0 2 5 | 0 1 0 6 | 0 1 1 7 | 0 1 2 8 | 0 2 0 9 | 0 2 1 10 | 0 2 2 11 | 1 0 0 12 | 1 0 1 13 | 1 0 2 14 | 1 1 0 15 | 1 1 1 16 | 1 1 2 17 | 1 2 0 18 | 1 2 1 19 | 1 2 2 20 | 2 0 0 21 | 2 0 1 22 | 2 0 2 23 | 2 1 0 24 | 2 1 1 25 | 2 1 2 26 | 2 2 0 27 | 2 2 1 28 | 2 2 2 29 | -------------------------------------------------------------------------------- /test/abc.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.xlsx -------------------------------------------------------------------------------- /test/abc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/abc.zip -------------------------------------------------------------------------------- /test/csv_import_http_server.py: -------------------------------------------------------------------------------- 1 | from http.server import HTTPServer, BaseHTTPRequestHandler 2 | from http import HTTPStatus 3 | import threading 4 | import time 5 | import urllib.error 6 | import urllib.request 7 | 8 | 9 | # noinspection PyPep8Naming 10 | class CSVTestRequestHandler(BaseHTTPRequestHandler): 11 | def do_GET(self): 12 | self.log_message("received %s %s", self.command, self.path) 13 | path = self.path 14 | if path == "/EXIT": 15 | self.send_response(HTTPStatus.OK) 16 | self.end_headers() 17 | threading.Thread(target=lambda: self.server.shutdown()).start() 18 | 19 | elif path == "/": 20 | self.send_response(HTTPStatus.OK) 21 | self.end_headers() 22 | 23 | elif path.startswith("/abc.csv"): 24 | send_bytes = b"a,b,c\n1,2,3\n" 25 | self.send_response(HTTPStatus.OK) 26 | self.end_headers() 27 | self.wfile.write(send_bytes) 28 | 29 | 30 | def run(server_class=HTTPServer, handler_class=CSVTestRequestHandler): 31 | server_address = ('', 8888) 32 | httpd = server_class(server_address, handler_class) 33 | httpd.serve_forever() 34 | 35 | 36 | def run_background_test_server(): 37 | p = threading.Thread(target=run) 38 | p.start() 39 | 40 | for tries_remaining in reversed(range(20)): 41 | try: 42 | with urllib.request.urlopen("http://localhost:8888/"): 43 | break 44 | except urllib.error.URLError: 45 | time.sleep(0.5) 46 | 47 | return p 48 | 49 | 50 | if __name__ == '__main__': 51 | run() 52 | -------------------------------------------------------------------------------- /test/data_types.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptmcg/littletable/77c64149e778923ce1feea141b279710243393a5/test/data_types.xlsx -------------------------------------------------------------------------------- /test/star_trek_tos_eps.jsonl: -------------------------------------------------------------------------------- 1 | {"date":"8 Sep 1966","title":"The Man Trap","description":"The crew encounters a deadly shapeshifter.","cast":{"Kirk":"William Shatner","Spock":"Leonard Nimoy"}} 2 | {"date":"15 Sep 1966","title":"Charlie X","description":"A teenager with dangerous powers boards the Enterprise.","cast":{"Kirk":"William Shatner","Spock":"Leonard Nimoy"}} 3 | {"date":"22 Sep 1966","title":"Where No Man Has Gone Before","description":"The Enterprise crew faces godlike powers in deep space.","cast":{"Kirk":"William Shatner","Spock":"Leonard Nimoy"}} 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = 3 | py3{9,10,11,12,13} 4 | minversion = 4.15.1 5 | skip_missing_interpreters = True 6 | 7 | [testenv] 8 | description = run the tests with pytest 9 | package = wheel 10 | wheel_build_env = .pkg 11 | deps = 12 | pytest>=6 13 | -r requirements.dev 14 | commands = 15 | pytest unit_tests.py {tty:--color=yes} {posargs} 16 | -------------------------------------------------------------------------------- /update_littletable_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, UTC 2 | from pathlib import Path 3 | from pyparsing import quoted_string 4 | 5 | nw = datetime.now(tz=UTC) 6 | now_string = '"%s"' % (nw.strftime("%d %b %Y %X")[:-3] + " UTC") 7 | print(now_string) 8 | 9 | quoted_time = quoted_string() 10 | quoted_time.set_parse_action(lambda: now_string) 11 | 12 | version_time = "__version_time__ = " + quoted_time 13 | 14 | pp_init = Path("littletable.py") 15 | orig_code = pp_init.read_text() 16 | new_code = version_time.transform_string(orig_code) 17 | pp_init.write_text(new_code) 18 | --------------------------------------------------------------------------------