-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add uv support and modernize CLI #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c833194
62fdfc3
ca0f10d
89dda5f
5fd0538
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| """ | ||
| Command line interface for fit_tool. | ||
|
|
||
| Copyright (c) 2017 Stages Cycling. All rights reserved. | ||
| """ | ||
| import argparse | ||
| import logging | ||
| import os | ||
|
|
||
| from fit_tool.fit_file import FitFile | ||
| from fit_tool.utils.logging import logger | ||
|
|
||
|
|
||
| def parse_args(): | ||
| """Parse command line arguments.""" | ||
| parser = argparse.ArgumentParser( | ||
| description="Tool for managing FIT files." | ||
| ) | ||
| parser.add_argument( | ||
| 'fitfile', | ||
| metavar='FILE', | ||
| help='FIT file to process' | ||
| ) | ||
| parser.add_argument( | ||
| '-v', '--verbose', | ||
| action='store_true', | ||
| help='specify verbose output' | ||
| ) | ||
| parser.add_argument("-o", "--output", help="Output filename.") | ||
| parser.add_argument("-l", "--log", help="Log filename.") | ||
| parser.add_argument( | ||
| "-t", "--type", | ||
| help="Output format type. Options: csv, fit." | ||
| ) | ||
|
|
||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point.""" | ||
| args = parse_args() | ||
|
|
||
| formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s %(message)s") | ||
|
|
||
| if args.log: | ||
| handler = logging.FileHandler(args.log) | ||
| handler.setFormatter(formatter) | ||
| logger.addHandler(handler) | ||
|
|
||
| if args.verbose: | ||
| handler = logging.StreamHandler() | ||
| handler.setFormatter(formatter) | ||
| logger.addHandler(handler) | ||
|
Comment on lines
50
to
53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if args.verbose:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt="%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler) |
||
| logger.setLevel(logging.DEBUG) | ||
| logger.info(f'Loading fit file {args.fitfile}...') | ||
|
|
||
| fit_file = FitFile.from_file(args.fitfile) | ||
|
|
||
| if args.type: | ||
| format_type = args.type | ||
| elif args.output: | ||
| _, out_ext = os.path.splitext(os.path.basename(args.output)) | ||
| format_type = out_ext.lstrip('.') | ||
| else: | ||
| format_type = 'csv' | ||
|
|
||
| basename_noext, _ = os.path.splitext(os.path.basename(args.fitfile)) | ||
| output_filename = args.output or f'{basename_noext}.{format_type}' | ||
|
|
||
| logger.info(f'Exporting fit file to {output_filename} as format {format_type}...') | ||
|
|
||
| if format_type == 'csv': | ||
| fit_file.to_csv(output_filename) | ||
| elif format_type == 'fit': | ||
| fit_file.to_file(output_filename) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """Tests for the command line interface.""" | ||
|
|
||
| import os | ||
| import tempfile | ||
| import unittest | ||
| from unittest.mock import patch | ||
|
|
||
| from fit_tool.cli import main, parse_args | ||
|
|
||
|
|
||
| class TestParseArgs(unittest.TestCase): | ||
|
|
||
| def test_parse_args_minimal(self): | ||
| with patch('sys.argv', ['fit-tool', 'test.fit']): | ||
| args = parse_args() | ||
| self.assertEqual(args.fitfile, 'test.fit') | ||
| self.assertFalse(args.verbose) | ||
| self.assertIsNone(args.output) | ||
| self.assertIsNone(args.log) | ||
| self.assertIsNone(args.type) | ||
|
|
||
| def test_parse_args_with_options(self): | ||
| with patch('sys.argv', ['fit-tool', 'test.fit', '-v', '-o', 'out.csv', '-l', 'log.txt', '-t', 'csv']): | ||
| args = parse_args() | ||
| self.assertEqual(args.fitfile, 'test.fit') | ||
| self.assertTrue(args.verbose) | ||
| self.assertEqual(args.output, 'out.csv') | ||
| self.assertEqual(args.log, 'log.txt') | ||
| self.assertEqual(args.type, 'csv') | ||
|
|
||
|
|
||
| class TestMain(unittest.TestCase): | ||
|
|
||
| def setUp(self): | ||
| self.test_dir = tempfile.mkdtemp() | ||
| self.test_fit_file = os.path.join( | ||
| os.path.dirname(__file__), | ||
| 'data', | ||
| 'sdk', | ||
| 'Activity.fit' | ||
| ) | ||
|
|
||
| def tearDown(self): | ||
| for f in os.listdir(self.test_dir): | ||
| os.remove(os.path.join(self.test_dir, f)) | ||
| os.rmdir(self.test_dir) | ||
|
|
||
| def test_main_convert_to_csv(self): | ||
| output_file = os.path.join(self.test_dir, 'output.csv') | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file, '-o', output_file]): | ||
| main() | ||
| self.assertTrue(os.path.exists(output_file)) | ||
|
|
||
| def test_main_convert_to_fit(self): | ||
| output_file = os.path.join(self.test_dir, 'output.fit') | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file, '-o', output_file, '-t', 'fit']): | ||
| main() | ||
| self.assertTrue(os.path.exists(output_file)) | ||
|
|
||
| def test_main_with_verbose(self): | ||
| output_file = os.path.join(self.test_dir, 'output.csv') | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file, '-v', '-o', output_file]): | ||
| main() | ||
| self.assertTrue(os.path.exists(output_file)) | ||
|
|
||
| def test_main_with_log_file(self): | ||
| output_file = os.path.join(self.test_dir, 'output.csv') | ||
| log_file = os.path.join(self.test_dir, 'test.log') | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file, '-o', output_file, '-l', log_file]): | ||
| main() | ||
| self.assertTrue(os.path.exists(output_file)) | ||
| self.assertTrue(os.path.exists(log_file)) | ||
|
|
||
| def test_main_default_output_filename(self): | ||
| original_cwd = os.getcwd() | ||
| try: | ||
| os.chdir(self.test_dir) | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file]): | ||
| main() | ||
| self.assertTrue(os.path.exists('Activity.csv')) | ||
| finally: | ||
| os.chdir(original_cwd) | ||
|
|
||
| def test_main_infer_format_from_output(self): | ||
| output_file = os.path.join(self.test_dir, 'output.fit') | ||
| with patch('sys.argv', ['fit-tool', self.test_fit_file, '-o', output_file]): | ||
| main() | ||
| self.assertTrue(os.path.exists(output_file)) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
argparse.FileType('r')opens the file in text mode, which can corrupt binary FIT files due to newline translation. It also causes the file to be opened twice: once byargparseand again byFitFile.from_file. It's better to pass the filename as a string and letFitFile.from_filehandle all file operations in the correct mode.