Metadata-Version: 2.1
Name: list2csv
Version: 1.2.0
Summary: A simple package intended to help write iterables of objects to CSV files
Home-page: https://github.com/James-Ansley/list2csv
Author: James Finnie-Ansley
License: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Topic :: Utilities
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE

# List2CSV

List2CSV is a simple package that helps with writing lists of objects to CSV files.

## Installation

list2csv can be downloaded from [pypi](https://pypi.org/project/list2csv/) or installed using pip:

    pip install list2csv

## Overview

The main class `Writer` takes a writable file as a parameter and can have various columns added with format specifiers.
Columns can either be defined by instance attribute names or functions that map an object to some value. For example:

```python
from dataclasses import dataclass
from statistics import mean

import list2csv


@dataclass
class Student:
    student_id: str
    test_mark: float
    lab_marks: list[float]


students = [
    Student('abcd123', 78.5, [92.3, 98, 100, 70]),
    Student('efgh456', 62, [98, 68.2, 0, 93.5]),
    Student('ijkl789', 100, [100, 100, 98.7, 100]),
]

with open('student_overview.csv', 'w') as f:
    writer = list2csv.Writer(f)
    writer.add_column('ID', 'student_id')
    writer.add_column('Test Mark', 'test_mark', '{:.2f}')
    writer.add_column('Average Lab Mark', lambda s: mean(s.lab_marks), '{:.2f}')

    writer.write_header()
    writer.write_all(students)
```

Would produce the following table:

|ID     |Test Mark|Average Lab Mark|
|-------|---------|----------------|
|abcd123|78.50    |90.08           |
|efgh456|62.00    |64.92           |
|ijkl789|100.00   |99.67           |

## Fields

Columns, added by `.add_column` or `.add_multi_column`, take a function that maps an object to a value **or** a string
that matches the name of an attribute of Type `T`.

In the above example, the `add_column` method is used to add the `student_id` and `test_mark` fields; both of which are
attribute names of `Student`. But the add field method is also used to add the `Average Lab Mark` field, which is _
computed_ using a lambda that maps a `Student` instance to a `float` representing the average lab mark for that student.

In the case of multi-columns, either a string representing an attribute name or a function that maps an object to an
iterable of values can be used.

### Writing Iterables

It may be desirable to quickly add iterables as a set of columns to the table. For this, the `.add_multi_column` method
can be used. This column expects iterables of a pre-defined length as specified by a `range` and will write them as
individual columns. For example, if we wanted to put the lab marks of each student in separate columns, we could do:

```python
with open('student_overview.csv', 'w') as f:
    writer = list2csv.Writer(f)
    writer.add_column('ID', 'student_id')
    writer.add_column('Test Mark', 'test_mark', '{:.2f}')
    writer.add_multi_column('Lab {}', 'lab_marks', range(1, 5), '{:.2f}')

    writer.write_header()
    writer.write_all(students)
```

Which would yield:

|ID     |Test Mark|Lab 1 |Lab 2 |Lab 3 |Lab 4 |
|-------|---------|------|------|------|------|
|abcd123|78.50    |92.30 |98.00 |100.00|70.00 |
|efgh456|62.00    |98.00 |68.20 |0.00  |93.50 |
|ijkl789|100.00   |100.00|100.00|98.70 |100.00|

Here the `name` parameter is a generic name that contains exactly one placeholder value. Upon writing the header, the
column names are generated by replacing the placeholder with the index of the column starting at value 1.

The number of values must be predefined. If an item in the resulting iterable does not have the same number of items,
a `ValueError` will be raised.

### Aggregator Columns

Columns have an `aggregate` parameter that indicates the field will be used in an aggregate column. These aggregate
columns can be added using the `.add_aggregator` method. For example, if we wanted to add the average lab mark to the
table, we could do:

```python
with open('student_overview.csv', 'w') as f:
    writer = list2csv.Writer(f)
    writer.add_column('ID', 'student_id')
    writer.add_column('Test Mark', 'test_mark', '{:.2f}')
    writer.add_multi_column('Lab {}', 'lab_marks',
                            range(1, 5), '{:.2f}', aggregate=True)
    writer.add_aggregator('Average Lab Mark', mean, '{:.2f}')

    writer.write_header()
    writer.write_all(students)
```

Which would yield:

|ID     |Test Mark|Lab 1 |Lab 2 |Lab 3 |Lab 4 |Average Lab Mark|
|-------|---------|------|------|------|------|----------------|
|abcd123|78.50    |92.30 |98.00 |100.00|70.00 |90.08           |
|efgh456|62.00    |98.00 |68.20 |0.00  |93.50 |64.92           |
|ijkl789|100.00   |100.00|100.00|98.70 |100.00|99.67           |

In this case, the use of the `.add_aggregator` method is trivial; however, it can be applied to several columns making
it useful in some instances. For example, if the 'TestMark' field was also flagged as an aggregate field, this would be
included in the average calculation for the 'Average Lab Mark' field.

The range parameter indicates the indices that will be used in the column headers, as well as how many columns will be
expected.

### Counters

To add auto incrementing values to the table, the `.add_counter` method can be used. This will increment values with a
given start and step value for each row written. For example:

```python
with open('student_overview.csv', 'w') as f:
    writer = list2csv.Writer(f)
    writer.add_counter('Student', start=1)
    writer.add_column('ID', 'student_id')
    writer.add_column('Test Mark', 'test_mark', '{:.2f}')
    writer.add_column('Average Lab Mark', lambda s: mean(s.lab_marks), '{:.2f}')

    writer.write_header()
    writer.write_all(students)
```

Would produce:

|Student|ID    |Test Mark|Average Lab Mark|
|-------|------|---------|----------------|
|1      |abcd123|78.50    |90.08           |
|2      |efgh456|62.00    |64.92           |
|3      |ijkl789|100.00   |99.67           |

Here the 'Student' column starts at one and increments by one for each row. Multiple counter columns can be added with
different start or step values if desired.

## Putting it all together

The following example adds a more complex student representation to demonstrate how these features may be used together.

```python
from dataclasses import dataclass
from statistics import mean

import list2csv


@dataclass
class Student:
    student_id: str
    test_1_mark: float
    test_2_mark: float
    assignment_marks: list[float]
    lab_marks: list[float]
    comments: list[str]


students = [
    Student('abcd123',
            78.5, 88,
            [84.5, 96, 87],
            [92.3, 98, 100, 70],
            ['Good', 'Needs work on classes']),
    Student('efgh456',
            62, 74,
            [70.5, 76, 80],
            [98, 68.2, 0, 93.5],
            ['Good', 'Needs work on formatting', 'Needs work on recursion']),
    Student('ijkl789',
            100, 99.5,
            [98.5, 100, 100],
            [100, 100, 98.7, 100],
            ['Excellent']),
]

with open('student_overview.csv', 'w') as f:
    writer = list2csv.Writer(f)
    writer.add_counter('Student', start=1)
    writer.add_column('ID', 'student_id')
    writer.add_column('Test 1', 'test_1_mark', '{:.2f}', aggregate=True)
    writer.add_column('Test 2', 'test_2_mark', '{:.2f}', aggregate=True)
    writer.add_aggregator('Av. Test Mark', mean, '{:.2f}')
    writer.add_multi_column('A{}', 'assignment_marks',
                            range(1, 4), '{:.2f}', aggregate=True)
    writer.add_aggregator('Av. Assignment Mark', mean, '{:.2f}')
    writer.add_multi_column('Lab {}', 'lab_marks',
                            range(1, 5), '{:.2f}', aggregate=True)
    writer.add_aggregator('Av. Lab Mark', mean, '{:.2f}')
    writer.add_column('Comments', lambda s: '\n'.join(s.comments))

    writer.write_header()
    writer.write_all(students)
```

Would produce:

|Student|ID    |Test 1|Test 2|Av. Test Mark|A1    |A2    |A3   |Av. Assignment Mark|Lab 1 |Lab 2 |Lab 3 |Lab 4|Av. Lab Mark                                         |Comments|
|-------|------|------|------|-------------|------|------|-----|-------------------|------|------|------|-----|-----------------------------------------------------|--------|
|1      |abcd123|78.50 |88.00 |83.25        |84.50 |96.00 |87.00|89.17              |92.30 |98.00 |100.00|70.00|90.08                                                |Good<br>Needs work on classes|
|2      |efgh456|62.00 |74.00 |68.00        |70.50 |76.00 |80.00|75.50              |98.00 |68.20 |0.00  |93.50|64.92                                                |Good<br>Needs work on formatting<br>Needs work on recursion|
|3      |ijkl789|100.00|99.50 |99.75        |98.50 |100.00|100.00|99.50              |100.00|100.00|98.70 |100.00|99.67                                                |Excellent|


