diff --git a/default.nix b/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..68776ea5039cd86eee667636d3c90dde93abba61 --- /dev/null +++ b/default.nix @@ -0,0 +1,45 @@ +{ pkgs ? import (fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/22.11.tar.gz"; + sha256 = "sha256:11w3wn2yjhaa5pv20gbfbirvjq6i3m7pqrq2msf0g7cv44vijwgw"; + }) {} +}: + +let + pyPkgs = pkgs.python3Packages; +in rec { + ut3_survival = pyPkgs.buildPythonPackage { + pname = "ut3_survival"; + version = "local"; + format = "pyproject"; + + src = pkgs.lib.sourceByRegex ./. [ + "pyproject\.toml" + "LICENSE" + "ut3_survival" + "ut3_survival/.*\.py" + "ut3_survival/cmd" + "ut3_survival/cmd/.*\.py" + ]; + buildInputs = with pyPkgs; [ + flit + ]; + propagatedBuildInputs = with pyPkgs; [ + xlrd + pandas + click + ]; + }; + + user-shell = pkgs.mkShell { + buildInputs = with pyPkgs; [ + ipython + ut3_survival + ]; + }; + + dev-shell = pkgs.mkShell { + buildInputs = with pyPkgs; [ + ipython + ] ++ ut3_survival.propagatedBuildInputs; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000000000000000000000000000000000..3476f0c6694484a4c86434a47a9135b3afdce056 --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1670952872, + "narHash": "sha256-tmhCNibwoviM+BHXBWUxO+XtAwbO84b2BHC4PrD7FrQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "16875b3e7be8380c29af192cc8ff1debae6d311a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000000000000000000000000000000000..241a8f8f9c2ce44c44b08481f797bc6f725705c9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,16 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs?tag=22.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = nixpkgs.legacyPackages.${system}; + in rec { + packages = import ./default.nix { inherit pkgs; }; + apps.xls-to-csv = flake-utils.lib.mkApp { drv = packages.ut3_survival; exePath = "/bin/xls-to-csv"; }; + defaultPackage = packages.ut3_survival; + } + ); +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..cdda2fc07f4b7c15b612c74c12a39dea6c106769 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +build-backend = "flit_core.buildapi" +requires = ["flit_core"] + +[project] +name = "ut3_survival" +version = "0.1.0" +description = "Set of tools to stay sane while teaching at UT3" +authors = [ + {name = "Millian Poquet", email="millian.poquet@irit.fr"}, +] +license = {file = "LICENSE"} +requires-python = ">=3.9" + +keywords = ["ut3"] + +classifiers = [ + "Topic :: Software Development", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", +] + +dependencies = [ + "pandas>=1.3.0", + "xlrd>=2.0.1", + "click>=8.0.0", +] + +[project.scripts] +xls-to-csv = "ut3_survival.cmd.xls_to_csv:main" diff --git a/ut3_survival/cmd/xls_to_csv.py b/ut3_survival/cmd/xls_to_csv.py new file mode 100644 index 0000000000000000000000000000000000000000..42df7c4911fca2c9d09bb33a8a78344b23224a80 --- /dev/null +++ b/ut3_survival/cmd/xls_to_csv.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import sys + +import click +import pandas + +from ut3_survival import realist + +@click.command() +@click.argument('xls_file', required=True, nargs=-1) +@click.option('-o', '--output', default=None, help='If set, parsed students are written as CSV to this file.') +def main(xls_file, output): + students = realist.read_parse_several_xls(xls_file) + + output_file = sys.stdout + if output is not None: + output_file = open(output, 'wt', encoding='utf-8') + + students_df = realist.student_entry_list_to_df(students) + students_df.sort_values(by=['group', 'lastname', 'firstname', 'id'], inplace=True) + students_df.to_csv(output_file, index=False) + +if __name__ == "__main__": + main() diff --git a/ut3_survival/realist.py b/ut3_survival/realist.py new file mode 100755 index 0000000000000000000000000000000000000000..7d5a0fa0ad82031d3b2ed5558915e9d6542aadf7 --- /dev/null +++ b/ut3_survival/realist.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +''' +Student list parser from REALIST (usually accessed via SGCE). +''' + +import sys +from collections import namedtuple + +import pandas +import xlrd + +student_columns = ['id', 'lastname', 'firstname', 'email', 'group'] +StudentEntry = namedtuple('StudentEntry', student_columns) + +def read_parse_xls(xls_filename: str) -> [StudentEntry]: + '''Read and parse a XLS file into a list of students.''' + students_xls = xlrd.open_workbook(xls_filename, logfile=sys.stderr) + group_names = students_xls.sheet_names() + group_names.pop(0) # first XLS sheet is useless + students = [] + + for group_name in group_names: + sheet = students_xls.sheet_by_name(group_name) + if sheet.cell_value(1,0) != "GROUPE : " + group_name: + raise AssertionError("SGCE's xls format has changed") + + for row in range(3, sheet.nrows): + students.append(StudentEntry( + id=sheet.cell_value(row,0), + lastname=sheet.cell_value(row,1), + firstname=sheet.cell_value(row,2), + email=sheet.cell_value(row,3), + group=group_name, + )) + return students + +def read_parse_several_xls(xls_filenames: [str]) -> [StudentEntry]: + '''read_parse_xls wrapper when several files are to be used.''' + all_students = [] + for xls_filename in xls_filenames: + try: + students = read_parse_xls(xls_filename) + all_students.extend(students) + except Exception as exception: + raise RuntimeError(f"could not read/parse xls file '{xls_filename}'") from exception + return all_students + +def student_entry_list_to_df(students: [StudentEntry]) -> pandas.DataFrame: + '''Create a DataFrame from a student list.''' + return pandas.DataFrame(students, columns=student_columns)