drac-notifier 0.1.0__py3-none-any.whl

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.
File without changes
File without changes
@@ -0,0 +1,99 @@
1
+ import logging
2
+ from typing import List
3
+ from typing import Optional
4
+
5
+ from icat_plus_client import ApiClient
6
+ from icat_plus_client import CatalogueApi
7
+ from icat_plus_client import Configuration
8
+ from icat_plus_client import SessionApi
9
+ from icat_plus_client.models import Credentials
10
+ from icat_plus_client.models import Investigation
11
+ from icat_plus_client.models import Investigationusers
12
+
13
+ from drac_notifier.config import ICAT_PLUS_PASSWORD
14
+ from drac_notifier.config import ICAT_PLUS_PLUGIN
15
+ from drac_notifier.config import ICAT_PLUS_SERVER
16
+ from drac_notifier.config import ICAT_PLUS_USERNAME
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class IcatClient:
22
+ """Client to interact with ICAT+"""
23
+
24
+ def __init__(self):
25
+ self.configuration = Configuration(host=ICAT_PLUS_SERVER)
26
+ self.username = ICAT_PLUS_USERNAME
27
+ self.password = ICAT_PLUS_PASSWORD
28
+ self.plugin = ICAT_PLUS_PLUGIN
29
+ self.session_id: Optional[str] = None
30
+
31
+ def login(self) -> Optional[str]:
32
+ """Authenticate and store session_id"""
33
+ try:
34
+ with ApiClient(self.configuration) as api_client:
35
+ api_instance = SessionApi(api_client)
36
+ cred = Credentials(
37
+ username=self.username,
38
+ password=self.password,
39
+ plugin=self.plugin,
40
+ )
41
+ response = api_instance.session_post(cred)
42
+ self.session_id = response.session_id
43
+ logger.info("Logged in to ICAT+, session_id obtained")
44
+ return self.session_id
45
+ except Exception as exc:
46
+ logger.exception("Unexpected error during login: %s", exc)
47
+ return None
48
+
49
+ def get_investigations(
50
+ self,
51
+ release_dates: Optional[str] = None,
52
+ ) -> List[Investigation]:
53
+ """Fetch investigations from ICAT+"""
54
+ if not self.session_id:
55
+ logger.error("No session_id. Please login first.")
56
+ return []
57
+
58
+ with ApiClient(self.configuration) as api_client:
59
+ api_instance = CatalogueApi(api_client)
60
+ try:
61
+ investigations = api_instance.catalogue_session_id_investigation_get(
62
+ session_id=self.session_id,
63
+ release_dates=release_dates,
64
+ )
65
+ logger.info(
66
+ "Fetched %d investigations",
67
+ len(investigations) if investigations else 0,
68
+ )
69
+ return investigations or []
70
+ except Exception as exc:
71
+ logger.exception("Unexpected error fetching investigations: %s", exc)
72
+ return []
73
+
74
+ def get_investigation_users(
75
+ self,
76
+ investigation_id: str,
77
+ ) -> List[Investigationusers]:
78
+ """Fetch investigation users from ICAT+"""
79
+ if not self.session_id:
80
+ logger.error("No session_id. Please login first.")
81
+ return []
82
+
83
+ with ApiClient(self.configuration) as api_client:
84
+ api_instance = CatalogueApi(api_client)
85
+ try:
86
+ users = api_instance.catalogue_session_id_investigation_id_investigation_id_investigationusers_get(
87
+ session_id=self.session_id,
88
+ investigation_id=investigation_id,
89
+ )
90
+ logger.info(
91
+ "Fetched %d investigation users",
92
+ len(users) if users else 0,
93
+ )
94
+ return users or []
95
+ except Exception as exc:
96
+ logger.exception(
97
+ "Unexpected error fetching investigation users: %s", exc
98
+ )
99
+ return []
@@ -0,0 +1,73 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import requests
5
+ from zeep import Client
6
+ from zeep.exceptions import Fault
7
+ from zeep.transports import Transport
8
+ from zeep.wsse.username import UsernameToken
9
+
10
+ from drac_notifier.config import SMIS_PASSWORD
11
+ from drac_notifier.config import SMIS_SERVER
12
+ from drac_notifier.config import SMIS_USERNAME
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SmisClient:
18
+ """Client to interact with UserPortal"""
19
+
20
+ def __init__(self):
21
+ self.server = SMIS_SERVER
22
+ self.username = SMIS_USERNAME
23
+ self.password = SMIS_PASSWORD
24
+
25
+ session = requests.Session()
26
+ session.auth = (self.username, self.password)
27
+ transport = Transport(session=session)
28
+
29
+ user_wsdl_url = f"{self.server}/SMISServer-ejb3/UserLookupWebService/UserLookupWebServiceBean?wsdl"
30
+ self.user_client = Client(
31
+ user_wsdl_url,
32
+ transport=transport,
33
+ wsse=UsernameToken(self.username, self.password),
34
+ )
35
+
36
+ smis_wsdl_url = (
37
+ f"{self.server}/SMISServer-ejb3/SMISWebService/SMISWebServiceBean?wsdl"
38
+ )
39
+ self.smis_client = Client(
40
+ smis_wsdl_url,
41
+ transport=transport,
42
+ wsse=UsernameToken(self.username, self.password),
43
+ )
44
+
45
+ def get_user_email(self, username: str) -> Optional[str]:
46
+ smis_username = username.split("/")[0]
47
+ try:
48
+ user_id = self.user_client.service.getUserIdForUserName(smis_username)
49
+
50
+ if not user_id:
51
+ logger.warning(f"No SMIS user ID found for {smis_username}")
52
+ return None
53
+
54
+ user_light_vo = self.smis_client.service.findUserByScientistPk(user_id)
55
+
56
+ if not user_light_vo:
57
+ logger.warning(f"No SMIS user found for ID {user_id}")
58
+ return None
59
+
60
+ email = getattr(user_light_vo, "scientistEmail", None)
61
+ return email
62
+
63
+ except Fault as fault:
64
+ logger.warning(
65
+ f"SMIS lookup failed for username={smis_username}: {fault.message}"
66
+ )
67
+ return None
68
+
69
+ except Exception:
70
+ logger.exception(
71
+ f"Unexpected error while retrieving email for {smis_username}"
72
+ )
73
+ return None
@@ -0,0 +1,23 @@
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv(override=True) # loads .env
6
+
7
+
8
+ def get_env(name: str) -> str:
9
+ value = os.environ.get(name)
10
+ if not value:
11
+ raise RuntimeError(f"Environment variable {name} is required but not set.")
12
+ return value
13
+
14
+
15
+ ICAT_PLUS_SERVER = get_env("ICAT_PLUS_SERVER")
16
+ ICAT_PLUS_USERNAME = get_env("ICAT_PLUS_USERNAME")
17
+ ICAT_PLUS_PASSWORD = get_env("ICAT_PLUS_PASSWORD")
18
+ ICAT_PLUS_PLUGIN = get_env("ICAT_PLUS_PLUGIN")
19
+ SMIS_SERVER = get_env("SMIS_SERVER")
20
+ SMIS_USERNAME = get_env("SMIS_USERNAME")
21
+ SMIS_PASSWORD = get_env("SMIS_PASSWORD")
22
+ BCC_EMAIL = get_env("BCC_EMAIL")
23
+ SENDER_EMAIL = get_env("SENDER_EMAIL")
File without changes
@@ -0,0 +1,67 @@
1
+ import logging
2
+ import smtplib
3
+ from email.mime.multipart import MIMEMultipart
4
+ from email.mime.text import MIMEText
5
+ from typing import List
6
+ from typing import Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class EmailService:
12
+ def __init__(
13
+ self,
14
+ smtp_host: str,
15
+ sender: str,
16
+ reply_to: str,
17
+ bcc: Optional[str] = None,
18
+ mode: str = "normal",
19
+ ):
20
+ if mode not in {"normal", "dry-run", "test"}:
21
+ raise ValueError(f"Invalid mode: {mode}")
22
+ self.mode = mode
23
+ self.smtp_host = smtp_host
24
+ self.sender = sender
25
+ self.reply_to = reply_to
26
+ self.bcc = bcc
27
+
28
+ def send_html_email(
29
+ self, subject: str, html_body: str, to_emails: List[str]
30
+ ) -> None:
31
+ if not to_emails:
32
+ logger.warning("[EMAIL][SKIP] No recipients")
33
+ return
34
+ recipients = list(dict.fromkeys(to_emails))
35
+ if self.mode == "dry-run":
36
+ logger.info(
37
+ f"[EMAIL][DRY_RUN] subject='{subject}' intended_recipients={recipients}"
38
+ )
39
+ return
40
+ msg = MIMEMultipart()
41
+ msg["Subject"] = subject
42
+ msg["From"] = self.sender
43
+ msg["Reply-To"] = self.reply_to
44
+ if self.mode == "test":
45
+ if not self.bcc:
46
+ logger.warning("[EMAIL][TEST_MODE] No BCC specified, skipping")
47
+ return
48
+ send_list = [self.bcc]
49
+ msg["To"] = self.bcc
50
+ logger.info(
51
+ f"[EMAIL][TEST] subject='{subject}' redirected_to_bcc={self.bcc} original_recipients={recipients}"
52
+ )
53
+ else:
54
+ send_list = recipients.copy()
55
+ msg["To"] = ", ".join(recipients)
56
+ if self.bcc:
57
+ send_list.append(self.bcc)
58
+ msg.attach(MIMEText(html_body, "html"))
59
+ try:
60
+ with smtplib.SMTP(self.smtp_host) as server:
61
+ server.sendmail(self.sender, send_list, msg.as_string())
62
+
63
+ logger.info(
64
+ f"[EMAIL][SENT] subject='{subject}' to={recipients} bcc={self.bcc}"
65
+ )
66
+ except Exception:
67
+ logger.exception("[EMAIL][ERROR] Failed to send email")
File without changes
@@ -0,0 +1,41 @@
1
+ EMBARGO_TEMPLATE = """
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>End of embargo email</title>
6
+ <style>
7
+ .text-footer {{
8
+ font-size: 0.8em;
9
+ color: #666;
10
+ }}
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <p>You are receiving this email because you have access to the ESRF data concerned.</p>
15
+
16
+ <p>
17
+ According to the <a href="{data_policy}">ESRF data policy</a>, the data concerned by the following experiment session will become public on the {release_date} given below:
18
+ </p>
19
+
20
+ <ul>
21
+ <li><b>Proposal ID:</b> {proposal}</li>
22
+ <li><b>Beamline:</b> {beamline}</li>
23
+ <li><b>Start date:</b> {start_date}</li>
24
+ <li><b>End date:</b> {end_date}</li>
25
+ <li><b>End of embargo date:</b> {release_date}</li>
26
+ </ul>
27
+
28
+ <p>
29
+ On this date, the data and metadata, including the electronic logbook and experiment session report, will all be made public via the session DOI landing page:
30
+ <a href="{doi_link}">{doi_link}</a>
31
+ </p>
32
+ <p>
33
+ You, as part of the proposal team (PI, CIs, and Users for this session), are responsible for the content of the data and metadata that will be made public. Please ensure that any sensitive and/or personal data are removed before this date, tidy the logbook and update the experiment report if required.
34
+ </p>
35
+ <p>
36
+ If an extension of the 3-year embargo period is required, the Principal Investigator must make the request by email to the Office of the Directors of Research (<a href="{mailto_link}">doroffice@esrf.fr</a>), clearly justifying the length of extension requested and why it is needed.
37
+ </p>
38
+ <p class="text-footer"><i>This email was automatically generated by the Data Portal.</i></p>
39
+ </body>
40
+ </html>
41
+ """
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+ from typing import Tuple
3
+ from urllib.parse import quote
4
+
5
+ from icat_plus_client.models import Investigation
6
+
7
+ from .end_embargo import EMBARGO_TEMPLATE
8
+
9
+
10
+ def build_embargo_email(inv: Investigation) -> Tuple[str, str]:
11
+ start_date = _format_date(inv.start_date)
12
+ end_date = _format_date(inv.end_date)
13
+ proposal = inv.name
14
+ beamline = inv.instrument.name
15
+ release_date = _format_date(inv.release_date)
16
+ proposal_subject = f"{proposal} on {beamline} From {start_date} to {end_date}"
17
+ mailto_subject = f"Embargo extension: {proposal_subject}"
18
+ mailto_subject_encoded = quote(mailto_subject)
19
+ mailto_link = f"mailto:doroffice@esrf.fr?subject={mailto_subject_encoded}"
20
+ subject = f"ESRF data end of embargo: {proposal_subject}"
21
+
22
+ html = EMBARGO_TEMPLATE.format(
23
+ proposal=proposal,
24
+ beamline=beamline,
25
+ start_date=start_date,
26
+ end_date=end_date,
27
+ release_date=release_date,
28
+ doi_link=f"https://doi.org/{inv.doi}",
29
+ data_policy="https://www.esrf.fr/files/live/sites/www/files/Infrastructure/Computing/ESRF-data-policy_20240101.pdf",
30
+ mailto_link=mailto_link,
31
+ )
32
+
33
+ return subject, html
34
+
35
+
36
+ def _format_date(date_str: str) -> str:
37
+ dt = datetime.fromisoformat(date_str)
38
+ return dt.strftime("%Y-%m-%d")
drac_notifier/main.py ADDED
@@ -0,0 +1,46 @@
1
+ import argparse
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ from drac_notifier.notifications.end_embargo import send_embargo_notifications
6
+
7
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s")
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(
13
+ description="DRAC Notifier tasks",
14
+ epilog="Example: python -m drac_notifier.main embargo 2026-02-01 --mode dry-run",
15
+ )
16
+ parser.add_argument("task", choices=["embargo"], help="Task to run")
17
+ parser.add_argument(
18
+ "date",
19
+ nargs="?",
20
+ default=None,
21
+ help="Reference date in YYYY-MM-DD format (optional)",
22
+ )
23
+ parser.add_argument(
24
+ "--mode",
25
+ choices=["normal", "dry-run", "test"],
26
+ default="normal",
27
+ help="Email sending mode: normal (send), dry-run (no send), test (send only to bcc)",
28
+ )
29
+
30
+ args = parser.parse_args()
31
+
32
+ if args.date:
33
+ try:
34
+ datetime.strptime(args.date, "%Y-%m-%d")
35
+ except ValueError:
36
+ logger.error("Invalid date format: %s. Expected YYYY-MM-DD", args.date)
37
+ return
38
+
39
+ if args.task == "embargo":
40
+ send_embargo_notifications(reference_date=args.date, mode=args.mode)
41
+ else:
42
+ logger.error("Unknown task: %s", args.task)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
File without changes
@@ -0,0 +1,121 @@
1
+ import logging
2
+ from datetime import date
3
+ from datetime import datetime
4
+ from typing import List
5
+ from typing import Optional
6
+ from typing import Set
7
+
8
+ from dateutil.relativedelta import relativedelta
9
+ from icat_plus_client.models import Investigation
10
+ from icat_plus_client.models import Investigationusers
11
+
12
+ from drac_notifier.clients.icat_plus import IcatClient
13
+ from drac_notifier.clients.smis import SmisClient
14
+ from drac_notifier.config import BCC_EMAIL
15
+ from drac_notifier.config import SENDER_EMAIL
16
+ from drac_notifier.email_notifier.service import EmailService
17
+ from drac_notifier.email_notifier.templates.templates import build_embargo_email
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def send_embargo_notifications(
23
+ reference_date: Optional[str], mode: str = "normal"
24
+ ) -> None:
25
+ logger.info("START EMBARGO NOTIFICATIONS")
26
+ client = IcatClient()
27
+ client.login()
28
+ smis_client = SmisClient()
29
+ email_service = EmailService(
30
+ smtp_host="localhost",
31
+ sender=SENDER_EMAIL,
32
+ reply_to=SENDER_EMAIL,
33
+ bcc=BCC_EMAIL,
34
+ mode=mode,
35
+ )
36
+
37
+ target_dates = _calculate_target_dates(reference_date)
38
+ release_dates = ",".join(target_dates)
39
+ logger.info(f"Release dates: {release_dates}")
40
+
41
+ investigations = client.get_investigations(release_dates=release_dates)
42
+ for inv in investigations:
43
+ logger.info(
44
+ f"[INVESTIGATION] id={inv.id} name={inv.name} release_date={inv.release_date}"
45
+ )
46
+ users = client.get_investigation_users(inv.id)
47
+ emails = set()
48
+ missing_users = []
49
+ for user in users:
50
+ email = smis_client.get_user_email(user.name)
51
+
52
+ if email:
53
+ emails.add(email)
54
+ logger.info(
55
+ f"[USER][OK] inv_id={inv.id} user={user.name} role={user.role} email={email}"
56
+ )
57
+ else:
58
+ missing_users.append(user.name)
59
+ logger.warning(
60
+ f"[USER][NO_EMAIL] inv_id={inv.id} user={user.name} role={user.role}"
61
+ )
62
+ # summary
63
+ skip = _log_summary_investigation(inv, users, emails, missing_users)
64
+ if skip:
65
+ continue
66
+
67
+ try:
68
+ subject, html = build_embargo_email(inv)
69
+ except Exception:
70
+ logger.exception(
71
+ f"[EMAIL][ERROR] Failed to build email for inv_id={inv.id}"
72
+ )
73
+ continue
74
+ logger.info(f"[EMAIL][PREPARE] inv_id={inv.id} recipients={len(emails)}")
75
+ email_service.send_html_email(
76
+ subject=subject,
77
+ html_body=html,
78
+ to_emails=list(emails),
79
+ )
80
+
81
+ logger.info("=" * 60)
82
+
83
+ logger.info("END Embargo notifications sent successfully")
84
+
85
+
86
+ def _log_summary_investigation(
87
+ inv: Investigation,
88
+ users: List[Investigationusers],
89
+ emails: Set[str],
90
+ missing_users: List[str],
91
+ ) -> bool:
92
+ logger.info(
93
+ f"[SUMMARY] inv_id={inv.id} total_users={len(users)} "
94
+ f"emails_found={len(emails)} missing={len(missing_users)}"
95
+ )
96
+ if missing_users:
97
+ logger.warning(
98
+ f"[SUMMARY][MISSING_EMAILS] inv_id={inv.id} users={missing_users}"
99
+ )
100
+ if not emails:
101
+ logger.warning(f"[SKIP] No emails for investigation {inv.id} name={inv.name}")
102
+ return True
103
+ return False
104
+
105
+
106
+ def _calculate_target_dates(reference_date: Optional[str] = None) -> List[str]:
107
+ """
108
+ Returns the target dates for embargo notifications:
109
+ - 1 month after reference_date
110
+ - 3 months after reference_date
111
+
112
+ If reference_date is None, use today.
113
+ """
114
+ if reference_date:
115
+ ref_date = datetime.strptime(reference_date, "%Y-%m-%d").date()
116
+ else:
117
+ ref_date = date.today()
118
+ one_month_date = ref_date + relativedelta(months=1)
119
+ three_month_date = ref_date + relativedelta(months=3)
120
+
121
+ return [one_month_date.isoformat(), three_month_date.isoformat()]
File without changes
@@ -0,0 +1,22 @@
1
+ from datetime import date
2
+
3
+ from drac_notifier.notifications.end_embargo import _calculate_target_dates
4
+
5
+
6
+ def test_calculate_target_dates_with_given_date():
7
+ result = _calculate_target_dates("2026-03-23")
8
+ assert result[0] == "2026-04-23"
9
+ assert result[1] == "2026-06-23"
10
+
11
+
12
+ def test_calculate_target_dates_with_default_today(monkeypatch):
13
+ class FakeDate(date):
14
+ @classmethod
15
+ def today(cls):
16
+ return cls(2026, 3, 23)
17
+
18
+ monkeypatch.setattr("drac_notifier.notifications.end_embargo.date", FakeDate)
19
+
20
+ result = _calculate_target_dates()
21
+ assert result[0] == "2026-04-23"
22
+ assert result[1] == "2026-06-23"
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: drac-notifier
3
+ Version: 0.1.0
4
+ Summary: Service for sending user notifications from ICAT based on data lifecycle events
5
+ Author-email: ESRF <marjolaine.bodin@esrf.fr>
6
+ License: # MIT License
7
+
8
+ **Copyright (c) 2026 European Synchrotron Radiation Facility**
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ this software and associated documentation files (the "Software"), to deal in
12
+ the Software without restriction, including without limitation the rights to
13
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Software, and to permit persons to whom the Software is furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://gitlab.esrf.fr/icat/drac-notifier/
28
+ Project-URL: Documentation, https://drac-notifier.readthedocs.io/
29
+ Project-URL: Repository, https://gitlab.esrf.fr/icat/drac-notifier/
30
+ Project-URL: Issues, https://gitlab.esrf.fr/icat/drac-notifier/issues
31
+ Project-URL: Changelog, https://gitlab.esrf.fr/icat/drac-notifier/-/blob/main/CHANGELOG.md
32
+ Keywords: icat
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Requires-Python: >=3.10
37
+ Description-Content-Type: text/markdown
38
+ License-File: LICENSE.md
39
+ Requires-Dist: icat-plus-client
40
+ Requires-Dist: python-dotenv>=1.0.0
41
+ Requires-Dist: zeep
42
+ Provides-Extra: test
43
+ Requires-Dist: pytest>=7; extra == "test"
44
+ Provides-Extra: dev
45
+ Requires-Dist: drac-notifier[test]; extra == "dev"
46
+ Requires-Dist: ruff; extra == "dev"
47
+ Provides-Extra: doc
48
+ Requires-Dist: drac-notifier[test]; extra == "doc"
49
+ Requires-Dist: sphinx>=4.5; extra == "doc"
50
+ Requires-Dist: sphinx-autodoc-typehints>=1.16; extra == "doc"
51
+ Requires-Dist: pydata-sphinx-theme; extra == "doc"
52
+ Requires-Dist: sphinx-copybutton; extra == "doc"
53
+ Dynamic: license-file
54
+
55
+ # drac-notifier
56
+
57
+ Service for sending user notifications from ICAT based on data lifecycle events.
58
+
59
+ ## End of embargo email
60
+
61
+ An email is sent to all users linked to an experiment session to notify them of the end of the embargo period, 3 months and 1 month before the end date.
@@ -0,0 +1,20 @@
1
+ drac_notifier/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ drac_notifier/config.py,sha256=ijXpFDodK3jganEp1sMPupidgBRfVGWqcaQFzwcvJtY,658
3
+ drac_notifier/main.py,sha256=bENRWl2neZq0Kht6oRpSp_O9qIIm4phFBKzwXwWwYWE,1353
4
+ drac_notifier/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ drac_notifier/clients/icat_plus.py,sha256=qOnNPOZhcjI9qPTWGTdlXpcsZ6DVGsi_1heg_kynwXs,3667
6
+ drac_notifier/clients/smis.py,sha256=KOmk-_8e5XMKE-fkyttymVmPEDTtSaVOktlK_cn06WQ,2290
7
+ drac_notifier/email_notifier/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ drac_notifier/email_notifier/service.py,sha256=8Dpk19G6Ben9c_VWsrFvN00J4A2ozKSg7LVSVFsvtFw,2196
9
+ drac_notifier/email_notifier/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ drac_notifier/email_notifier/templates/end_embargo.py,sha256=q8tfruYfBKSOgxYkad6Msp4UxzRjq4QI9RoUurKjSTI,1754
11
+ drac_notifier/email_notifier/templates/templates.py,sha256=jCW-aKd_vanLq1Itr3i_6AwdhtvrjwnrtVPCYZieXwY,1320
12
+ drac_notifier/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ drac_notifier/notifications/end_embargo.py,sha256=7W3MEFfj5SAJEUeOalPFVKY-5v8m2ijxTREupVpPCTQ,3925
14
+ drac_notifier/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ drac_notifier/tests/test_release_date.py,sha256=x4Xx4VVC3qo_BBlLjPlGIOJxxyOMqYLMtGtBareTxQo,652
16
+ drac_notifier-0.1.0.dist-info/licenses/LICENSE.md,sha256=jhnnemwVcGW_Dc_FbaaD12x2Vz9mIqjX_MtyKrjw3gQ,1102
17
+ drac_notifier-0.1.0.dist-info/METADATA,sha256=CkB62rh4Bt_Oqj67qVuCM3MoivUIP-hcgXDzqZeqOYs,2912
18
+ drac_notifier-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
19
+ drac_notifier-0.1.0.dist-info/top_level.txt,sha256=Bdg8lupC6ATzXJikNnOMJga8kpIx1-3FXlUj6i31cPI,14
20
+ drac_notifier-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,20 @@
1
+ # MIT License
2
+
3
+ **Copyright (c) 2026 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 @@
1
+ drac_notifier