minedrac 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. minedrac-0.0.1/LICENSE.md +20 -0
  2. minedrac-0.0.1/PKG-INFO +27 -0
  3. minedrac-0.0.1/README.md +56 -0
  4. minedrac-0.0.1/pyproject.toml +73 -0
  5. minedrac-0.0.1/setup.cfg +4 -0
  6. minedrac-0.0.1/src/authentication/cli.py +30 -0
  7. minedrac-0.0.1/src/data/__init__.py +0 -0
  8. minedrac-0.0.1/src/data/dataset.py +64 -0
  9. minedrac-0.0.1/src/data/instrument.py +39 -0
  10. minedrac-0.0.1/src/data/investigation.py +65 -0
  11. minedrac-0.0.1/src/data/parcel.py +59 -0
  12. minedrac-0.0.1/src/data/sample.py +37 -0
  13. minedrac-0.0.1/src/data/session.py +53 -0
  14. minedrac-0.0.1/src/invoicing/__init__.py +0 -0
  15. minedrac-0.0.1/src/invoicing/cli.py +148 -0
  16. minedrac-0.0.1/src/invoicing/duration.py +152 -0
  17. minedrac-0.0.1/src/logger_utils/__init__.py +0 -0
  18. minedrac-0.0.1/src/logger_utils/csv_formatter.py +19 -0
  19. minedrac-0.0.1/src/logger_utils/time_formatter.py +10 -0
  20. minedrac-0.0.1/src/minedrac/__init__.py +0 -0
  21. minedrac-0.0.1/src/minedrac.egg-info/PKG-INFO +27 -0
  22. minedrac-0.0.1/src/minedrac.egg-info/SOURCES.txt +42 -0
  23. minedrac-0.0.1/src/minedrac.egg-info/dependency_links.txt +1 -0
  24. minedrac-0.0.1/src/minedrac.egg-info/entry_points.txt +2 -0
  25. minedrac-0.0.1/src/minedrac.egg-info/requires.txt +24 -0
  26. minedrac-0.0.1/src/minedrac.egg-info/top_level.txt +10 -0
  27. minedrac-0.0.1/src/performance/__init__.py +0 -0
  28. minedrac-0.0.1/src/performance/cli.py +88 -0
  29. minedrac-0.0.1/src/stats/__init__.py +0 -0
  30. minedrac-0.0.1/src/stats/cli.py +14 -0
  31. minedrac-0.0.1/src/stats/datasets.py +11 -0
  32. minedrac-0.0.1/src/stats/investigation_parameters.py +29 -0
  33. minedrac-0.0.1/src/stats/samples.py +12 -0
  34. minedrac-0.0.1/src/stats/technique/cli.py +107 -0
  35. minedrac-0.0.1/src/stats/technique/technique.py +148 -0
  36. minedrac-0.0.1/src/stats/volume/cli.py +192 -0
  37. minedrac-0.0.1/src/stats/volume/volume.py +144 -0
  38. minedrac-0.0.1/src/tests/invoicing/test_calculate_duration.py +175 -0
  39. minedrac-0.0.1/src/tests/statistics/test_calculate_volume.py +77 -0
  40. minedrac-0.0.1/src/tests/statistics/test_technique.py +76 -0
  41. minedrac-0.0.1/src/tests_integration/test_technique.py +43 -0
  42. minedrac-0.0.1/src/utils/__init__.py +0 -0
  43. minedrac-0.0.1/src/utils/timing.py +15 -0
  44. minedrac-0.0.1/src/utils/units.py +0 -0
@@ -0,0 +1,20 @@
1
+ # MIT License
2
+
3
+ **Copyright (c) 2021 European Synchrotron Radiation Facility**
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: minedrac
3
+ Version: 0.0.1
4
+ Requires-Python: >=3.11
5
+ License-File: LICENSE.md
6
+ Requires-Dist: icat-plus-client
7
+ Requires-Dist: typer
8
+ Requires-Dist: python-dotenv
9
+ Requires-Dist: pandas
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest; extra == "test"
12
+ Requires-Dist: pytest-spec; extra == "test"
13
+ Requires-Dist: pytest-dotenv; extra == "test"
14
+ Provides-Extra: docs
15
+ Requires-Dist: sphinx<9,>=7; extra == "docs"
16
+ Requires-Dist: myst-parser<5,>=3; extra == "docs"
17
+ Requires-Dist: sphinx-click>=6; extra == "docs"
18
+ Provides-Extra: dev
19
+ Requires-Dist: typer>=0.20; extra == "dev"
20
+ Requires-Dist: pandas>=2.3; extra == "dev"
21
+ Requires-Dist: black>=25; extra == "dev"
22
+ Requires-Dist: flake8>=4; extra == "dev"
23
+ Requires-Dist: ruff; extra == "dev"
24
+ Requires-Dist: pytest; extra == "dev"
25
+ Requires-Dist: pytest-spec; extra == "dev"
26
+ Requires-Dist: pytest-dotenv; extra == "dev"
27
+ Dynamic: license-file
@@ -0,0 +1,56 @@
1
+ # MineDRAC
2
+
3
+ ## Configuration
4
+
5
+ ### Logging
6
+
7
+ You can manage the logging level with an env variable:
8
+
9
+ ```
10
+ export LOG_LEVEL=DEBUG
11
+ ```
12
+
13
+ ### ICAT+
14
+
15
+ MineDRAC is configured to use the official ESRF icat+ server by default: https://icatplus.esrf.fr
16
+
17
+ It can be configured to use any other server via a configuration `.env` file or export the variable `icat_plus`
18
+ Example:
19
+
20
+ ```
21
+ export icat_plus_server="http://localhost:8000"
22
+ ```
23
+
24
+ You can find out which server is pointing to by using the command:
25
+
26
+ ```
27
+ > minedrac config icat_plus
28
+ http://localhost:8083
29
+
30
+ ```
31
+
32
+ ### Commands
33
+
34
+ #### login
35
+
36
+ This command allows to get the token of a DB user (this will not work for user accounts). For getting your token you can access it via the data portal.
37
+
38
+ ```
39
+ > minedrac login authentication --username reader --password ****
40
+ c99d6e5d-096b-45f1-bde3-c4322fe15251
41
+
42
+ ```
43
+
44
+ #### Retrieve
45
+
46
+ This command family allows to retrieve data from ICAT via ICAT+.
47
+
48
+ ##### Dataset
49
+
50
+ This method will return the json serialization of the datasets with the filters applied.
51
+
52
+ You can list the filters by typing:
53
+
54
+ ```
55
+ minedrac retrieve dataset --help
56
+ ```
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "minedrac"
7
+ version = "0.0.1"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "icat-plus-client",
11
+ "typer",
12
+ "python-dotenv",
13
+ "pandas"
14
+ ]
15
+ [project.optional-dependencies]
16
+ test = [
17
+ "pytest",
18
+ "pytest-spec",
19
+ "pytest-dotenv"
20
+ ]
21
+ docs = [
22
+ "sphinx>=7,<9",
23
+ "myst-parser>=3,<5",
24
+ "sphinx-click>=6",
25
+ ]
26
+ dev = [
27
+ "typer >=0.20",
28
+ "pandas >=2.3",
29
+ "black >=25",
30
+ "flake8 >=4",
31
+ "ruff",
32
+ "pytest",
33
+ "pytest-spec",
34
+ "pytest-dotenv"
35
+ ]
36
+
37
+
38
+ [tool.setuptools]
39
+ package-dir = {"" = "src"}
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.black]
45
+ line-length = 110
46
+ target-version = ["py39", "py310", "py311"] # adjust to your versions
47
+ include = '\.pyi?$'
48
+ exclude = '''
49
+ /(
50
+ \.git
51
+ | \.mypy_cache
52
+ | \.pytest_cache
53
+ | build
54
+ | dist
55
+ )/
56
+ '''
57
+
58
+ [tool.ruff]
59
+ line-length = 110
60
+ target-version = "py311"
61
+
62
+ [tool.ruff.lint]
63
+ select = [
64
+ "E", # errors
65
+ "F", # pyflakes
66
+ "I", # import sorting
67
+ "B", # flake8-bugbear
68
+ "UP", # pyupgrade
69
+ ]
70
+
71
+
72
+ [project.scripts]
73
+ minedrac = "cli:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ import typer
2
+
3
+ from data import session
4
+
5
+ authentication_app = typer.Typer(help="Authentication commands")
6
+
7
+
8
+ @authentication_app.command("authenticate", help="Gets the ICAT's token based on username and password")
9
+ def authenticate_cli(
10
+ authenticator: str = typer.Option(
11
+ "db",
12
+ "-a",
13
+ "--authenticator",
14
+ help="Plugin authentication. it can be db or esrf. Default is db",
15
+ ),
16
+ username: str = typer.Option(..., "-u", "--username", help="ICAT username"),
17
+ password: str = typer.Option(..., "-p", "--password", help="ICAT password"),
18
+ ):
19
+ """Retrieve investigations by session_id."""
20
+ session_response = session.get_session(authenticator, username, password)
21
+ typer.echo(session_response)
22
+
23
+
24
+ @authentication_app.command("info", help="Gets the ICAT's session information based on a valid ICAT's token")
25
+ def get_session_info(
26
+ token: str = typer.Option(..., "-t", "--token", help="ICAT token"),
27
+ ):
28
+ """Retrieve investigations by session_id."""
29
+ session_response = session.get_info(token)
30
+ typer.echo(session_response)
File without changes
@@ -0,0 +1,64 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.dataset import Dataset
6
+
7
+ from config import configuration
8
+
9
+ # Load .env if needed
10
+ load_dotenv()
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_datasets(
18
+ session_id: str,
19
+ investigation_ids: str | None = None,
20
+ instrument_name: str | None = None,
21
+ dataset_ids: str | None = None,
22
+ dataset_type: str | None = None,
23
+ nested: str | None = None,
24
+ sample_id: str | None = None,
25
+ limit: int | None = None,
26
+ skip: int | None = None,
27
+ search: str | None = None,
28
+ sort_by: str | None = None,
29
+ sort_order: str | None = None,
30
+ ) -> list[Dataset]:
31
+ with icat_plus_client.ApiClient(configuration) as api_client:
32
+ api_instance = icat_plus_client.CatalogueApi(api_client)
33
+ try:
34
+ datasets: list[Dataset] = api_instance.catalogue_session_id_dataset_get(
35
+ session_id,
36
+ investigation_ids=investigation_ids,
37
+ dataset_ids=dataset_ids,
38
+ sample_id=sample_id,
39
+ instrument_name=instrument_name,
40
+ limit=limit,
41
+ skip=skip,
42
+ search=search,
43
+ dataset_type=dataset_type,
44
+ nested=nested,
45
+ sort_by=sort_by,
46
+ sort_order=sort_order,
47
+ )
48
+
49
+ if datasets:
50
+ logger.debug("Fetched %d datasets successfully.", len(datasets))
51
+ return datasets
52
+ else:
53
+ logger.debug("No datasets found for the given parameters.")
54
+ return []
55
+
56
+ except icat_plus_client.rest.ApiException as api_exc:
57
+ logger.error(
58
+ "API Exception when CatalogueApi->catalogue_session_id_dataset_get: %s",
59
+ api_exc,
60
+ )
61
+ except Exception as e:
62
+ logger.exception("Unexpected error while fetching datasets: %s", e)
63
+
64
+ return []
@@ -0,0 +1,39 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.instrument import Instrument
6
+
7
+ from config import configuration
8
+
9
+ # Load .env if needed
10
+ load_dotenv()
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_instruments() -> list[Instrument]:
18
+ """Fetch all instruments from ICAT Catalogue."""
19
+ with icat_plus_client.ApiClient(configuration) as api_client:
20
+ api_instance = icat_plus_client.CatalogueApi(api_client)
21
+ try:
22
+ instruments: list[Instrument] = api_instance.catalogue_instruments_get()
23
+
24
+ if instruments:
25
+ logger.debug("Fetched %d instruments successfully.", len(instruments))
26
+ return instruments
27
+ else:
28
+ logger.warning("No instruments found in the catalogue.")
29
+ return []
30
+
31
+ except icat_plus_client.rest.ApiException as api_exc:
32
+ logger.error(
33
+ "API Exception when calling CatalogueApi->catalogue_instruments_get: %s",
34
+ api_exc,
35
+ )
36
+ except Exception as e:
37
+ logger.exception("Unexpected error while fetching instruments: %s", e)
38
+
39
+ return []
@@ -0,0 +1,65 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.investigation import Investigation
6
+
7
+ from config import configuration
8
+
9
+ # Load .env if needed
10
+ load_dotenv()
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_investigation_by_id(
18
+ session_id: str,
19
+ investigation_id: str | None = None,
20
+ instrument_name: str | None = None,
21
+ investigation_name: str | None = None,
22
+ filter: str | None = None,
23
+ start_date: str | None = None,
24
+ end_date: str | None = None,
25
+ limit: int | None = None,
26
+ skip: int | None = None,
27
+ search: str | None = None,
28
+ sort_by: str | None = None,
29
+ sort_order: str | None = None,
30
+ with_has_access: bool | None = None,
31
+ ) -> list[Investigation]:
32
+ """Fetch investigations by session ID with optional filters."""
33
+
34
+ with icat_plus_client.ApiClient(configuration) as api_client:
35
+ api_instance = icat_plus_client.CatalogueApi(api_client)
36
+ try:
37
+ investigations: list[Investigation] = api_instance.catalogue_session_id_investigation_get(
38
+ session_id=session_id,
39
+ instrument_name=instrument_name,
40
+ investigation_name=investigation_name,
41
+ ids=investigation_id,
42
+ filter=filter,
43
+ start_date=start_date,
44
+ end_date=end_date,
45
+ limit=limit,
46
+ skip=skip,
47
+ search=search,
48
+ sort_by=sort_by,
49
+ sort_order=sort_order,
50
+ with_has_access=with_has_access,
51
+ )
52
+
53
+ if investigations:
54
+ logger.debug("Fetched %d investigations successfully.", len(investigations))
55
+ return investigations
56
+ else:
57
+ logger.debug("No investigations found for the given parameters.")
58
+ return []
59
+
60
+ except icat_plus_client.rest.ApiException as api_exc:
61
+ logger.error(f"API Exception CatalogueApi->catalogue_session_id_investigation_get: {api_exc}")
62
+ except Exception as e:
63
+ logger.exception("Unexpected error while fetching investigations: %s", e)
64
+
65
+ return []
@@ -0,0 +1,59 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.item import Item
6
+ from icat_plus_client.models.parcel import Parcel
7
+
8
+ from config import configuration
9
+
10
+ # Load .env if needed
11
+ load_dotenv()
12
+
13
+ # Setup logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def get_parcels_by(
19
+ token: str,
20
+ investigation_id: str | None = None,
21
+ ) -> list[Parcel]:
22
+ with icat_plus_client.ApiClient(configuration) as api_client:
23
+ # Create an instance of the API class
24
+ api_instance = icat_plus_client.TrackingApi(api_client)
25
+
26
+ try:
27
+ # Lists shipments associated to an investigation
28
+ shipments = api_instance.tracking_session_id_shipment_get(
29
+ investigation_id=investigation_id, session_id=token
30
+ )
31
+ if len(shipments) == 1:
32
+ parcels = api_instance.tracking_session_id_parcel_get(
33
+ shipments[0].id, token, "", investigation_id=investigation_id
34
+ )
35
+ logger.debug("Fetched %d parcels successfully.", len(parcels))
36
+ if parcels:
37
+ return parcels
38
+ except Exception as e:
39
+ logger.error(f"Exception when TrackingApi->tracking_session_id_parcel_get: {e}")
40
+ return []
41
+
42
+
43
+ def get_declared_samples_by(
44
+ token: str,
45
+ investigation_id: str | None = None,
46
+ ) -> list[Item]:
47
+ try:
48
+ parcels = get_parcels_by(token, investigation_id)
49
+ samples = []
50
+ for parcel in parcels:
51
+ for item in parcel.content:
52
+ if item.type == "CONTAINER":
53
+ samples.extend(item.content)
54
+ else:
55
+ samples.extend(item)
56
+ return samples
57
+ except Exception as e:
58
+ logger.error(f"Exception when get_declared_samples_by: {e}")
59
+ return []
@@ -0,0 +1,37 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.sample import Sample
6
+
7
+ from config import configuration
8
+
9
+ # Load .env if needed
10
+ load_dotenv()
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_samples_by(
18
+ token: str,
19
+ investigation_id: str | None = None,
20
+ ) -> list[Sample]:
21
+ with icat_plus_client.ApiClient(configuration) as api_client:
22
+ # Create an instance of the API class
23
+ api_instance = icat_plus_client.CatalogueApi(api_client)
24
+ investigation_id = investigation_id
25
+ try:
26
+ # Gets samples
27
+ samples = api_instance.catalogue_session_id_samples_get(
28
+ token, investigation_id=str(investigation_id)
29
+ )
30
+ if samples:
31
+ logger.debug("Fetched %d samples successfully.", len(samples))
32
+ return samples
33
+ else:
34
+ logger.debug("No samples found for the given parameters.")
35
+ return []
36
+ except Exception as e:
37
+ print(f"Exception when CatalogueApi->catalogue_session_id_samples_get: {e}")
@@ -0,0 +1,53 @@
1
+ import logging
2
+
3
+ import icat_plus_client
4
+ from dotenv import load_dotenv
5
+ from icat_plus_client.models.credentials import Credentials
6
+ from icat_plus_client.models.session import Session
7
+
8
+ from config import configuration
9
+
10
+ # Load .env if needed
11
+ load_dotenv()
12
+
13
+ # Setup logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def get_session(authenticator: str, username: str, password: str):
19
+ """Get the sessionId"""
20
+ with icat_plus_client.ApiClient(configuration) as api_client:
21
+ api_instance = icat_plus_client.SessionApi(api_client)
22
+ try:
23
+ cred = Credentials(
24
+ username=username,
25
+ password=password,
26
+ plugin=authenticator,
27
+ )
28
+ api_response = api_instance.session_post(cred)
29
+ return api_response.session_id
30
+ except icat_plus_client.rest.ApiException as api_exc:
31
+ logger.error(
32
+ "API Exception when calling SessionApi->session_post: %s",
33
+ api_exc,
34
+ )
35
+ except Exception as e:
36
+ logger.exception("Unexpected error while logging in: %s", e)
37
+
38
+ return []
39
+
40
+
41
+ def get_info(token: str) -> Session:
42
+ """Get the sessionId"""
43
+ with icat_plus_client.ApiClient(configuration) as api_client:
44
+ # Create an instance of the API class
45
+ api_instance = icat_plus_client.SessionApi(api_client)
46
+
47
+ try:
48
+ # Gets information about the session in ICAT
49
+ api_response: Session = api_instance.session_session_id_get(token)
50
+ return api_response
51
+
52
+ except Exception as e:
53
+ print(f"Exception when calling SessionApi->session_session_id_get: {e}\n")
File without changes
@@ -0,0 +1,148 @@
1
+ import csv
2
+ import sys
3
+ from datetime import datetime
4
+
5
+ import typer
6
+
7
+ from data import dataset, investigation
8
+ from data.parcel import get_declared_samples_by, get_parcels_by
9
+ from invoicing import duration
10
+ from stats.volume.volume import get_volumes_by_investigation
11
+
12
+ invoicing_app = typer.Typer(help="Invoicing commands")
13
+ report_invoicing_app = typer.Typer(help="report commands")
14
+ invoicing_app.add_typer(report_invoicing_app, name="report")
15
+
16
+
17
+ @report_invoicing_app.command("report", help="Generates a report for invoicong for each investigation")
18
+ def invoicing_report(
19
+ token: str = typer.Option(..., "-t", "--token", help="ICAT session ID"),
20
+ investigation_ids: str | None = typer.Option(
21
+ None,
22
+ "--investigation-ids",
23
+ "-i",
24
+ help="Comma separated list of investigation IDs (optional)",
25
+ ),
26
+ instrument_name: str | None = typer.Option(None, help="Filter by instrument name"),
27
+ investigation_name: str | None = typer.Option(None, help="Filter by instrument name"),
28
+ start_date: str = typer.Option(
29
+ None,
30
+ "-s",
31
+ "--start-date",
32
+ help="Start date range. Format is YYYY-MM-DD. Example: 2019-01-01",
33
+ ),
34
+ end_date: str = typer.Option(
35
+ None,
36
+ "-e",
37
+ "--end-date",
38
+ help="End date range. Format is YYYY-MM-DD. Example: 2019-01-01",
39
+ ),
40
+ unit: str = typer.Option(
41
+ "GB",
42
+ "-u",
43
+ "--unit",
44
+ help="It can be MB, GB or TB (otherwise will return in GB)",
45
+ ),
46
+ merge_gap_minutes: int = typer.Option(
47
+ 20,
48
+ "-g",
49
+ "--gap",
50
+ help="Gap in minutes to merge datasets. Less than 'gap' minutes the datasets time will be merged",
51
+ ),
52
+ output: str = typer.Option(
53
+ None,
54
+ "-o",
55
+ "--output",
56
+ help="CSV output file path. Example: myfile.csv",
57
+ ),
58
+ log: str = typer.Option(
59
+ None,
60
+ "-l",
61
+ "--log",
62
+ help="CSV log output file path. Example: myfile_log.csv",
63
+ ),
64
+ ignore_longer_than: int = typer.Option(
65
+ None,
66
+ "-ig",
67
+ "--ignore",
68
+ help="The time in hours that will be taken into account to omit a dataset as considered an error.",
69
+ ),
70
+ ):
71
+ investigations = investigation.get_investigation_by_id(
72
+ session_id=token,
73
+ investigation_id=investigation_ids,
74
+ start_date=start_date,
75
+ end_date=end_date,
76
+ instrument_name=instrument_name,
77
+ investigation_name=investigation_name,
78
+ )
79
+
80
+ fields = [
81
+ "proposal",
82
+ "investigation_id",
83
+ "beamline",
84
+ "start_date",
85
+ "end_date",
86
+ "booked_time",
87
+ "samples_collected",
88
+ "samples_declared",
89
+ "parcels",
90
+ "volume",
91
+ "raw_volume",
92
+ "processed_volume",
93
+ "effective_time",
94
+ "total_time",
95
+ ]
96
+
97
+ writers = [csv.DictWriter(sys.stdout, fieldnames=fields, delimiter="\t")]
98
+ # If file provided, also write to the file
99
+ if output:
100
+ f = open(output, "w", newline="")
101
+ writers.append(csv.DictWriter(f, fieldnames=fields, delimiter="\t"))
102
+ else:
103
+ f = None
104
+
105
+ # Write headers on all writers
106
+ for w in writers:
107
+ w.writeheader()
108
+
109
+ for inv in investigations:
110
+ (total, raw, processed) = get_volumes_by_investigation([inv], unit)
111
+ samples_declared = get_declared_samples_by(token, str(inv.id))
112
+ parcels = get_parcels_by(token, str(inv.id))
113
+ start = datetime.fromisoformat(inv.start_date)
114
+ end = datetime.fromisoformat(inv.end_date) if inv.end_date is not None else start
115
+ row = {
116
+ "proposal": inv.name,
117
+ "investigation_id": inv.id,
118
+ "beamline": inv.instrument.name,
119
+ "start_date": start.strftime("%Y-%m-%d %H:%M:%S"),
120
+ "end_date": end.strftime("%Y-%m-%d %H:%M:%S"),
121
+ "booked_time": end - start,
122
+ "volume": f"{total} {unit}",
123
+ "raw_volume": f"{raw} {unit}",
124
+ "processed_volume": f"{processed} {unit}",
125
+ }
126
+
127
+ """Calculate total dataset duration for a session."""
128
+ datasets = dataset.get_datasets(
129
+ session_id=token,
130
+ investigation_ids=str(inv.id),
131
+ dataset_type="acquisition",
132
+ )
133
+
134
+ samples_collected = sorted({d.sample_name for d in datasets if getattr(d, "sample_name", None)})
135
+ row["samples_collected"] = len(samples_collected)
136
+ row["samples_declared"] = len(samples_declared)
137
+ row["parcels"] = len(parcels)
138
+ row["total_time"] = duration.sum_dataset_durations(
139
+ datasets, merge_gap_minutes, log, ignore_longer_than
140
+ )
141
+ row["effective_time"] = duration.sum_dataset_durations(datasets, 0, None, ignore_longer_than)
142
+
143
+ for w in writers:
144
+ w.writerow(row)
145
+
146
+
147
+ click_invoicing_app = typer.main.get_command(invoicing_app)
148
+ report_click_invoicing_app = typer.main.get_command(report_invoicing_app)