diff --git a/.gitignore b/.gitignore
index 7bb58e6..bcf84e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ build/
*.lock
bk/
site/
+update.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12a9c7a..0b24b9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+v 0.6.0 [17-Dec-2023]
+---------------------
+ - Add control styles.
+
v 0.5.0 [07-Dec-2023]
---------------------
- Add acctions to controls.
diff --git a/VERSION b/VERSION
index 8f0916f..a918a2a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.5.0
+0.6.0
diff --git a/docs/en/docs/img/tools_msg_01.png b/docs/en/docs/img/tools_msg_01.png
new file mode 100644
index 0000000..764b754
Binary files /dev/null and b/docs/en/docs/img/tools_msg_01.png differ
diff --git a/docs/en/docs/img/tools_msg_02.png b/docs/en/docs/img/tools_msg_02.png
new file mode 100644
index 0000000..699c501
Binary files /dev/null and b/docs/en/docs/img/tools_msg_02.png differ
diff --git a/docs/en/docs/img/tools_msg_03.png b/docs/en/docs/img/tools_msg_03.png
new file mode 100644
index 0000000..0addf55
Binary files /dev/null and b/docs/en/docs/img/tools_msg_03.png differ
diff --git a/docs/en/docs/img/tools_msg_04.png b/docs/en/docs/img/tools_msg_04.png
new file mode 100644
index 0000000..5092179
Binary files /dev/null and b/docs/en/docs/img/tools_msg_04.png differ
diff --git a/docs/en/docs/img/tools_msg_05.png b/docs/en/docs/img/tools_msg_05.png
new file mode 100644
index 0000000..b1e3489
Binary files /dev/null and b/docs/en/docs/img/tools_msg_05.png differ
diff --git a/docs/en/docs/img/tools_msg_06.png b/docs/en/docs/img/tools_msg_06.png
new file mode 100644
index 0000000..972ce4c
Binary files /dev/null and b/docs/en/docs/img/tools_msg_06.png differ
diff --git a/docs/en/docs/tools/datetime.md b/docs/en/docs/tools/datetime.md
new file mode 100644
index 0000000..6d66fe4
--- /dev/null
+++ b/docs/en/docs/tools/datetime.md
@@ -0,0 +1,177 @@
+
+!!! tip "Atención"
+
+ La fecha inicial en Calc y en Python son diferentes.
+
+
+
+### **today**
+
+Obtener la fecha actual.
+
+```py
+ d = app.dates
+ app.msgbox(d.today)
+```
+
+
+
+### **now**
+
+Obtener la fecha y hora actuales.
+
+```py
+ d = app.dates
+ app.msgbox(d.now)
+```
+
+
+
+### **time**
+
+Obtener la hora actual.
+
+```py
+ d = app.dates
+ app.msgbox(d.now.time())
+```
+
+
+
+### **epoch**
+
+Obtener el [tiempo Unix][1]
+
+```py
+ d = app.dates
+ app.msgbox(d.epoch)
+```
+
+
+
+### **date**
+
+Devolver una fecha
+
+```py
+ d = app.dates
+
+ date = d.date(1974, 1, 15)
+ app.msgbox(date)
+```
+
+
+
+### **time**
+
+Devolver una hora
+
+```py
+ d = app.dates
+
+ time = d.time(10, 20, 15)
+ app.msgbox(time)
+```
+
+
+
+### **datetime**
+
+Devolver fecha y hora
+
+```py
+ d = app.dates
+
+ dt = d.datetime(1974, 1, 15, 10, 11, 12)
+ app.msgbox(dt)
+```
+
+
+
+### **str_to_date**
+
+Convertir una cadena en fecha. Mira este [excelente recurso][2]
+
+```py
+ d = app.dates
+
+ cadena = '1974-01-15'
+ plantilla = '%Y-%m-%d'
+ fecha = d.str_to_date(cadena, plantilla)
+ app.msgbox(fecha)
+ app.msgbox(type(fecha))
+```
+
+Para obtener un valor válido para establecer en una celda de Calc.
+
+```py
+ d = app.dates
+
+ cadena = '1974-01-15'
+ plantilla = '%Y-%m-%d'
+ fecha = d.str_to_date(cadena, plantilla, True)
+ app.msgbox(fecha)
+ app.msgbox(type(fecha))
+```
+
+
+
+### **calc_to_date**
+
+Convierte el valor de una celda en una fecha Python, por ejemplo, la fecha inicial configurada en Calc.
+
+```py
+ d = app.dates
+
+ valor_en_celda = 0
+ fecha = d.calc_to_date(valor_en_celda)
+ app.msgbox(fecha)
+ app.msgbox(type(fecha))
+```
+
+
+
+### **sleep**
+
+Pausar la ejecución por X segundos.
+
+!!! tip inline end "Atención"
+
+ La pausa es bloqueante.
+
+```py
+ d = app.dates
+
+ app.sleep(3)
+ app.msgbox('Fin')
+```
+
+
+
+### **start** y **end**
+
+Medir tiempo en segundos.
+
+```py
+ d = app.dates
+
+ d.start()
+ app.sleep(5)
+ seconds = d.end()
+ app.msgbox(seconds)
+```
+
+Regresar timedelta en vez de segundos.
+
+```py
+ d = app.dates
+
+ d.start()
+ app.sleep(5)
+ td = d.end(False)
+ app.msgbox(td)
+```
+
+
+[1]: https://es.wikipedia.org/wiki/Tiempo_Unix
+[2]: https://strftime.org
diff --git a/docs/en/docs/tools/index.md b/docs/en/docs/tools/index.md
new file mode 100644
index 0000000..6ed32ea
--- /dev/null
+++ b/docs/en/docs/tools/index.md
@@ -0,0 +1,123 @@
+---
+title: Information
+---
+
+Remember, import first the library.
+
+```py
+import easymacro as app
+```
+
+
+
+## About PC
+
+
+
+### **OS**
+
+Get Operate System.
+```py
+app.msgbox(app.OS)
+```
+
+
+
+### **DESKTOP**
+
+Get desktop type, only GNU/Linux.
+```py
+app.msgbox(app.DESKTOP)
+```
+
+
+
+### **PC**
+
+Get PC name.
+```py
+app.msgbox(app.PC)
+```
+
+
+
+### **USER**
+
+Get user name.
+```py
+app.msgbox(app.USER)
+```
+
+
+
+### **IS_WIN**
+
+If OS is Windows.
+```py
+app.msgbox(app.IS_WIN)
+```
+
+
+
+### **IS_MAC**
+
+IF OS is MAC
+```py
+app.msgbox(app.IS_MAC)
+```
+
+
+
+## About LibreOffice
+
+### **NAME**
+
+Application name.
+```py
+app.msgbox(app.NAME)
+```
+
+
+
+### **VERSION**
+
+Version.
+```py
+app.msgbox(app.VERSION)
+```
+
+
+
+### **LANG**
+
+Language
+```py
+app.msgbox(app.LANG)
+```
+
+
+
+### **LANGUAGE**
+
+Language with variant.
+```py
+app.msgbox(app.LANGUAGE)
+```
+
+
+
+### **IS_APPIMAGE**
+
+If LibreOffice use by AppImage.
+```py
+app.msgbox(app.IS_APPIMAGE)
+```
+
+
+
+### **IS_FLATPAK**
+
+If LibreOffice is use by FlatPak.
+```py
+app.msgbox(app.IS_FLATPAK)
+```
diff --git a/docs/en/docs/tools/messages.md b/docs/en/docs/tools/messages.md
new file mode 100644
index 0000000..6fd464e
--- /dev/null
+++ b/docs/en/docs/tools/messages.md
@@ -0,0 +1,75 @@
+## Message Box
+
+### **msgbox**
+
+Show standard message.
+```py
+ message = 'Fucking World'
+ title = 'My Macro'
+ app.msgbox(message, title)
+```
+
+![msgbox](../img/tools_msg_01.png)
+
+
+
+### **warning**
+
+Show message with warning icon.
+```py
+ message = 'Caution, this action is dangerous'
+ title = 'My Macro'
+ app.warning(message, title)
+```
+
+![warning](../img/tools_msg_02.png)
+
+
+
+### **errorbox**
+
+Show message with error icon.
+```py
+ message = 'ERROR: contact support'
+ title = 'My Macro'
+ app.errorbox(message, title)
+```
+
+![error](../img/tools_msg_03.png)
+
+
+
+### **question**
+
+Ask a question by showing the interrogation icon and displaying the command buttons `Yes` and `No`. The answer is always True if user select `yes` and False otherwise.
+```py
+ message = 'Python is easy?'
+ title = 'My Macro'
+ result = app.question(message, title)
+ app.msgbox(result)
+```
+
+![question](../img/tools_msg_04.png)
+
+
+
+### **inputbox**
+
+Shows a message to user, allowing to capture an answer.
+```py
+ message = 'Capture your name'
+ name = app.inputbox(message)
+ app.msgbox(name)
+```
+
+![inputbox](../img/tools_msg_05.png)
+
+To hide on screen what the user typing, util for request passwords.
+```py
+ message = 'Type your password'
+ echochar = '*'
+ password = app.inputbox(message, echochar=echochar)
+ app.msgbox(password)
+```
+
+![inputbox](../img/tools_msg_06.png)
\ No newline at end of file
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index 00d4ccf..edbc449 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -5,6 +5,10 @@ nav:
- Home: index.md
- Install: install.md
- Debug: debug.md
+ - Tools:
+ - tools/index.md
+ - Messages: tools/messages.md
+ - Dates and time: tools/datetime.md
theme:
name: material
locale: en
@@ -33,8 +37,8 @@ markdown_extensions:
extra:
alternate:
- name: English
- link: /
+ link: /easymacro
lang: en
- name: Español
- link: /langs/es
+ link: /easymacro/langs/es
lang: es
diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml
index 2392800..dfcf9e4 100644
--- a/docs/es/mkdocs.yml
+++ b/docs/es/mkdocs.yml
@@ -69,8 +69,8 @@ markdown_extensions:
extra:
alternate:
- name: English
- link: /
+ link: /easymacro
lang: en
- name: Español
- link: /langs/es
+ link: /easymacro/langs/es
lang: es
diff --git a/source/easymacro/constants.py b/source/easymacro/constants.py
new file mode 100644
index 0000000..bef7b98
--- /dev/null
+++ b/source/easymacro/constants.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+# ~ Is de sum of:
+# ~ https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html
+
+from com.sun.star.sheet import CellFlags
+
+ONLY_DATA = 31
+
+ALL = 1023
diff --git a/source/easymacro/easycalc.py b/source/easymacro/easycalc.py
index 4dab252..5153660 100644
--- a/source/easymacro/easycalc.py
+++ b/source/easymacro/easycalc.py
@@ -16,75 +16,13 @@ from .easyevents import EventsRangeSelectionListener, LOEvents
from .easyshape import LOShapes, LOShape
from .easydrawpage import LODrawPage
from .easyforms import LOForms
+from .easystyles import LOStyleFamilies
SECONDS_DAY = 60 * 60 * 24
ONLY_VALUES = CellFlags.VALUE + CellFlags.DATETIME + CellFlags.STRING
-class LOCellStyle():
-
- def __init__(self, obj):
- self._obj = obj
-
- def __str__(self):
- return f'CellStyle: {self.name}'
-
- @property
- def obj(self):
- return self._obj
-
- @property
- def name(self):
- return self.obj.Name
-
- @property
- def properties(self):
- properties = self.obj.PropertySetInfo.Properties
- data = {p.Name: getattr(self.obj, p.Name) for p in properties}
- return data
- @properties.setter
- def properties(self, values):
- set_properties(self.obj, values)
-
-
-class LOCellStyles():
-
- def __init__(self, obj, doc):
- self._obj = obj
- self._doc = doc
-
- def __len__(self):
- return len(self.obj)
-
- def __getitem__(self, index):
- return LOCellStyle(self.obj[index])
-
- def __setitem__(self, key, value):
- self.obj[key] = value
-
- def __delitem__(self, key):
- if not isinstance(key, str):
- key = key.Name
- del self.obj[key]
-
- def __contains__(self, item):
- return item in self.obj
-
- @property
- def obj(self):
- return self._obj
-
- @property
- def names(self):
- return self.obj.ElementNames
-
- def new(self, name: str):
- obj = self._doc.create_instance('com.sun.star.style.CellStyle')
- self.obj[name] = obj
- return LOCellStyle(obj)
-
-
# ~ IsFiltered,
# ~ IsManualPageBreak,
# ~ IsStartOfNewPage
@@ -939,6 +877,11 @@ class LOCalc(LODocument):
"""Get class events"""
return LOEvents(self.obj.Events)
+ @property
+ def styles(self):
+ ci = self.obj.createInstance
+ return LOStyleFamilies(self.obj.StyleFamilies, ci)
+
def ranges(self):
"""Create ranges container"""
obj = self._create_instance('com.sun.star.sheet.SheetCellRanges')
@@ -1111,5 +1054,4 @@ class LOCalc(LODocument):
return self.cell_styles
@property
def cell_styles(self):
- obj = self.obj.StyleFamilies['CellStyles']
- return LOCellStyles(obj, self)
+ return self.styles['CellStyles']
diff --git a/source/easymacro/easymain.py b/source/easymacro/easymain.py
index 6765153..9b89b75 100644
--- a/source/easymacro/easymain.py
+++ b/source/easymacro/easymain.py
@@ -24,6 +24,7 @@ from com.sun.star.beans import PropertyValue, NamedValue, StringPair
from com.sun.star.datatransfer import XTransferable, DataFlavor
from com.sun.star.ui.dialogs import TemplateDescription
+from .constants import ALL
from .messages import MESSAGES
@@ -31,6 +32,8 @@ __all__ = [
'ALL',
'DESKTOP',
'INFO_DEBUG',
+ 'IS_APPIMAGE',
+ 'IS_FLATPAK',
'IS_MAC',
'IS_WIN',
'LANG',
@@ -63,13 +66,12 @@ PC = platform.node()
USER = getpass.getuser()
IS_WIN = OS == 'Windows'
IS_MAC = OS == 'Darwin'
-
-
-ALL = 1023
+IS_FLATPAK = bool(os.getenv("FLATPAK_ID", ""))
+IS_APPIMAGE = bool(os.getenv("APPIMAGE", ""))
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
-LOG_DATE = '%d/%m/%Y %H:%M:%S'
+LOG_DATE = '%Y/%m/%d %H:%M:%S'
if IS_WIN:
logging.addLevelName(logging.ERROR, 'ERROR')
logging.addLevelName(logging.DEBUG, 'DEBUG')
@@ -149,7 +151,6 @@ day = get_app_config(node, 'DD')
DATE_OFFSET = datetime.date(year, month, day).toordinal()
_info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path)
-# ~ doc
INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}"
@@ -221,6 +222,12 @@ def set_properties(model, properties):
return
+def get_properties(obj):
+ properties = obj.PropertySetInfo.Properties
+ values = {p.Name: getattr(obj, p.Name) for p in properties}
+ return values
+
+
# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61
class classproperty:
diff --git a/source/easymacro/easystyles.py b/source/easymacro/easystyles.py
new file mode 100644
index 0000000..5ea2621
--- /dev/null
+++ b/source/easymacro/easystyles.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+
+
+from .easymain import log, BaseObject, set_properties, get_properties
+
+
+STYLE_FAMILIES = 'StyleFamilies'
+
+
+class LOBaseStyles(BaseObject):
+
+ def __init__(self, obj, create_instance=None):
+ super().__init__(obj)
+ self._create_intance = create_instance
+
+ def __len__(self):
+ return self.obj.Count
+
+ def __contains__(self, item):
+ return self.obj.hasByName(item)
+
+ def __getitem__(self, index):
+ if index in self:
+ style = self.obj.getByName(index)
+ else:
+ raise IndexError
+
+ if self.NAME == STYLE_FAMILIES:
+ s = LOStyles(style, index, self._create_intance)
+ else:
+ s = LOStyle(style)
+ return s
+
+ def __iter__(self):
+ self._index = 0
+ return self
+
+ def __next__(self):
+ if self._index < self.obj.Count:
+ style = self[self.names[self._index]]
+ else:
+ raise StopIteration
+
+ self._index += 1
+ return style
+
+ @property
+ def names(self):
+ return self.obj.ElementNames
+
+
+class LOStyle():
+ NAME = 'Style'
+
+ def __init__(self, obj):
+ self._obj = obj
+
+ def __str__(self):
+ return f'Style: {self.name}'
+
+ def __contains__(self, item):
+ return hasattr(self.obj, item)
+
+ def __setattr__(self, name, value):
+ if name != '_obj':
+ self.obj.setPropertyValue(name, value)
+ else:
+ super().__setattr__(name, value)
+
+ def __getattr__(self, name):
+ return self.obj.getPropertyValue(name)
+
+ @property
+ def obj(self):
+ return self._obj
+
+ @property
+ def name(self):
+ return self.obj.Name
+
+ @property
+ def is_in_use(self):
+ return self.obj.isInUse()
+
+ @property
+ def is_user_defined(self):
+ return self.obj.isUserDefined()
+
+ @property
+ def properties(self):
+ return get_properties(self.obj)
+ @properties.setter
+ def properties(self, values):
+ set_properties(self.obj, values)
+
+
+class LOStyles(LOBaseStyles):
+ NAME = 'Styles'
+
+ def __init__(self, obj, type_style, create_instance):
+ super().__init__(obj)
+ self._type_style = type_style
+ self._create_instance = create_instance
+
+ def __str__(self):
+ return f'Styles: {self._type_style}'
+
+ def __setitem__(self, key, value):
+ if key in self:
+ style = self.obj.getByName(key)
+ else:
+ name = f'com.sun.star.style.{self._type_style[:-1]}'
+ style = self._create_instance(name)
+ self.obj.insertByName(key, style)
+ set_properties(style, value)
+
+ def __delitem__(self, key):
+ self.obj.removeByName(key)
+
+
+class LOStyleFamilies(LOBaseStyles):
+ NAME = STYLE_FAMILIES
+
+ def __init__(self, obj, create_instance):
+ super().__init__(obj, create_instance)
+
+ def __str__(self):
+ return f'Style Families: {self.names}'
+
+ def _validate_name(self, name):
+ if not name.endswith('Styles'):
+ name = f'{name}Styles'
+ return name
+
+ def __contains__(self, item):
+ return self.obj.hasByName(self._validate_name(item))
+
+ def __getitem__(self, index):
+ if isinstance(index, int):
+ index = self.names[index]
+ else:
+ index = self._validate_name(index)
+ return super().__getitem__(index)
diff --git a/source/easymacro/easywriter.py b/source/easymacro/easywriter.py
index 1fce072..22e20df 100644
--- a/source/easymacro/easywriter.py
+++ b/source/easymacro/easywriter.py
@@ -1,11 +1,123 @@
#!/usr/bin/env python3
+from .easymain import log, BaseObject
from .easydoc import LODocument
+class LOWriterTextPortion(BaseObject):
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def __str__(self):
+ return 'Writer: TextPortion'
+
+ @property
+ def string(self):
+ return self.obj.String
+
+
+class LOWriterParagraph(BaseObject):
+ TEXT_PORTION = 'SwXTextPortion'
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def __str__(self):
+ return 'Writer: Paragraph'
+
+ def __iter__(self):
+ self._iter = iter(self.obj)
+ return self
+
+ def __next__(self):
+ obj = next(self._iter)
+ type_obj = obj.ImplementationName
+ if type_obj == self.TEXT_PORTION:
+ obj = LOWriterTextPortion(obj)
+ return obj
+
+ @property
+ def string(self):
+ return self.obj.String
+
+ @property
+ def cursor(self):
+ return self.obj.Text.createTextCursorByRange(self.obj)
+
+
+class LOWriterTextRange(BaseObject):
+ PARAGRAPH = 'SwXParagraph'
+
+ def __init__(self, obj):
+ super().__init__(obj)
+
+ def __str__(self):
+ return 'Writer: TextRange'
+
+ def __getitem__(self, index):
+ for i, v in enumerate(self):
+ if index == i:
+ return v
+ if index > i:
+ raise IndexError
+
+ def __iter__(self):
+ self._enum = self.obj.createEnumeration()
+ return self
+
+ def __next__(self):
+ if self._enum.hasMoreElements():
+ obj = self._enum.nextElement()
+ type_obj = obj.ImplementationName
+ if type_obj == self.PARAGRAPH:
+ obj = LOWriterParagraph(obj)
+ else:
+ raise StopIteration
+ return obj
+
+ @property
+ def string(self):
+ return self.obj.String
+
+ @property
+ def cursor(self):
+ return self.obj.Text.createTextCursorByRange(self.obj)
+
+
+class LOWriterTextRanges(BaseObject):
+
+ def __init__(self, obj):
+ super().__init__(obj)
+ # ~ self._doc = doc
+ # ~ self._paragraphs = [LOWriterTextRange(p, doc) for p in obj]
+
+ def __str__(self):
+ return 'Writer: TextRanges'
+
+ def __len__(self):
+ return self.obj.Count
+
+ def __getitem__(self, index):
+ return LOWriterTextRange(self.obj[index])
+
+ def __iter__(self):
+ self._index = 0
+ return self
+
+ def __next__(self):
+ try:
+ obj = LOWriterTextRange(self.obj[self._index])
+ except IndexError:
+ raise StopIteration
+
+ self._index += 1
+ return obj
+
+
class LOWriter(LODocument):
- _type = 'writer'
TEXT_RANGES = 'SwXTextRanges'
+ _type = 'writer'
def __init__(self, obj):
super().__init__(obj)
@@ -17,3 +129,25 @@ class LOWriter(LODocument):
@zoom.setter
def zoom(self, value):
self._view_settings.ZoomValue = value
+
+ @property
+ def selection(self):
+ """Get current seleccion"""
+ sel = None
+ selection = self.obj.CurrentSelection
+ type_obj = selection.ImplementationName
+
+ if type_obj == self.TEXT_RANGES:
+ sel = LOWriterTextRanges(selection)
+ if len(sel) == 1:
+ sel = sel[0]
+ else:
+ log.debug(type_obj)
+ log.debug(selection)
+ sel = selection
+
+ return sel
+
+ @property
+ def string(self):
+ return self._obj.Text.String
diff --git a/source/easymacro/utils.py b/source/easymacro/utils.py
new file mode 100644
index 0000000..e75677a
--- /dev/null
+++ b/source/easymacro/utils.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+
+# ~ https://github.com/pyca/cryptography/blob/main/src/cryptography/fernet.py#L27
+
+import base64
+import binascii
+import os
+import time
+import typing
+
+
+_MAX_CLOCK_SKEW = 60
+
+
+class InvalidSignature(Exception):
+ pass
+
+
+class InvalidToken(Exception):
+ pass
+
+
+class Fernet:
+ def __init__(
+ self,
+ key: bytes | str,
+ backend: typing.Any = None,
+ ) -> None:
+ try:
+ key = base64.urlsafe_b64decode(key)
+ except binascii.Error as exc:
+ raise ValueError(
+ "Fernet key must be 32 url-safe base64-encoded bytes."
+ ) from exc
+ if len(key) != 32:
+ raise ValueError(
+ "Fernet key must be 32 url-safe base64-encoded bytes."
+ )
+
+
+ self._signing_key = key[:16]
+ self._encryption_key = key[16:]
+
+
+ @classmethod
+ def generate_key(cls) -> bytes:
+ return base64.urlsafe_b64encode(os.urandom(32))
+
+
+ def encrypt(self, data: bytes) -> bytes:
+ return self.encrypt_at_time(data, int(time.time()))
+
+
+ def encrypt_at_time(self, data: bytes, current_time: int) -> bytes:
+ iv = os.urandom(16)
+ return self._encrypt_from_parts(data, current_time, iv)
+
+
+ def _encrypt_from_parts(
+ self, data: bytes, current_time: int, iv: bytes
+ ) -> bytes:
+ utils._check_bytes("data", data)
+
+
+ padder = padding.PKCS7(algorithms.AES.block_size).padder()
+ padded_data = padder.update(data) + padder.finalize()
+ encryptor = Cipher(
+ algorithms.AES(self._encryption_key),
+ modes.CBC(iv),
+ ).encryptor()
+ ciphertext = encryptor.update(padded_data) + encryptor.finalize()
+
+
+ basic_parts = (
+ b"\x80"
+ + current_time.to_bytes(length=8, byteorder="big")
+ + iv
+ + ciphertext
+ )
+
+
+ h = HMAC(self._signing_key, hashes.SHA256())
+ h.update(basic_parts)
+ hmac = h.finalize()
+ return base64.urlsafe_b64encode(basic_parts + hmac)
+
+
+ def decrypt(self, token: bytes | str, ttl: int | None = None) -> bytes:
+ timestamp, data = Fernet._get_unverified_token_data(token)
+ if ttl is None:
+ time_info = None
+ else:
+ time_info = (ttl, int(time.time()))
+ return self._decrypt_data(data, timestamp, time_info)
+
+
+ def decrypt_at_time(
+ self, token: bytes | str, ttl: int, current_time: int
+ ) -> bytes:
+ if ttl is None:
+ raise ValueError(
+ "decrypt_at_time() can only be used with a non-None ttl"
+ )
+ timestamp, data = Fernet._get_unverified_token_data(token)
+ return self._decrypt_data(data, timestamp, (ttl, current_time))
+
+
+ def extract_timestamp(self, token: bytes | str) -> int:
+ timestamp, data = Fernet._get_unverified_token_data(token)
+ # Verify the token was not tampered with.
+ self._verify_signature(data)
+ return timestamp
+
+
+ @staticmethod
+ def _get_unverified_token_data(token: bytes | str) -> tuple[int, bytes]:
+ if not isinstance(token, (str, bytes)):
+ raise TypeError("token must be bytes or str")
+
+
+ try:
+ data = base64.urlsafe_b64decode(token)
+ except (TypeError, binascii.Error):
+ raise InvalidToken
+
+
+ if not data or data[0] != 0x80:
+ raise InvalidToken
+
+
+ if len(data) < 9:
+ raise InvalidToken
+
+
+ timestamp = int.from_bytes(data[1:9], byteorder="big")
+ return timestamp, data
+
+
+ def _verify_signature(self, data: bytes) -> None:
+ h = HMAC(self._signing_key, hashes.SHA256())
+ h.update(data[:-32])
+ try:
+ h.verify(data[-32:])
+ except InvalidSignature:
+ raise InvalidToken
+
+
+ def _decrypt_data(
+ self,
+ data: bytes,
+ timestamp: int,
+ time_info: tuple[int, int] | None,
+ ) -> bytes:
+ if time_info is not None:
+ ttl, current_time = time_info
+ if timestamp + ttl < current_time:
+ raise InvalidToken
+
+
+ if current_time + _MAX_CLOCK_SKEW < timestamp:
+ raise InvalidToken
+
+
+ self._verify_signature(data)
+
+
+ iv = data[9:25]
+ ciphertext = data[25:-32]
+ decryptor = Cipher(
+ algorithms.AES(self._encryption_key), modes.CBC(iv)
+ ).decryptor()
+ plaintext_padded = decryptor.update(ciphertext)
+ try:
+ plaintext_padded += decryptor.finalize()
+ except ValueError:
+ raise InvalidToken
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
+
+
+ unpadded = unpadder.update(plaintext_padded)
+ try:
+ unpadded += unpadder.finalize()
+ except ValueError:
+ raise InvalidToken
+ return unpadded
diff --git a/source/tests/test_config.py b/source/tests/test_config.py
index 1cc6e69..fa9579d 100644
--- a/source/tests/test_config.py
+++ b/source/tests/test_config.py
@@ -9,6 +9,6 @@ USER = 'elmau'
IS_WIN = False
IS_MAC = False
-LIBO_VERSION = '7.4'
+LIBO_VERSION = '7.6'
LANGUAGE = 'en-US'
LANG = 'en'