agoras-common 2.0.0__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.
- agoras_common-2.0.0/PKG-INFO +79 -0
- agoras_common-2.0.0/README.rst +46 -0
- agoras_common-2.0.0/setup.cfg +4 -0
- agoras_common-2.0.0/setup.py +52 -0
- agoras_common-2.0.0/src/agoras/__init__.py +3 -0
- agoras_common-2.0.0/src/agoras/common/__init__.py +45 -0
- agoras_common-2.0.0/src/agoras/common/logger.py +134 -0
- agoras_common-2.0.0/src/agoras/common/utils.py +96 -0
- agoras_common-2.0.0/src/agoras/common/version.py +36 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/PKG-INFO +79 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/SOURCES.txt +15 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/dependency_links.txt +1 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/not-zip-safe +1 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/requires.txt +2 -0
- agoras_common-2.0.0/src/agoras_common.egg-info/top_level.txt +1 -0
- agoras_common-2.0.0/tests/test_logger.py +243 -0
- agoras_common-2.0.0/tests/test_utils.py +259 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agoras-common
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Common utilities and logging for Agoras
|
|
5
|
+
Home-page: https://github.com/LuisAlejandro/agoras
|
|
6
|
+
Author: Luis Alejandro Martínez Faneyth
|
|
7
|
+
Author-email: luis@luisalejandro.org
|
|
8
|
+
Keywords: utilities,logging,social networks
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
21
|
+
Requires-Dist: requests==2.33.1
|
|
22
|
+
Requires-Dist: beautifulsoup4==4.14.3
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
agoras-common
|
|
35
|
+
=============
|
|
36
|
+
|
|
37
|
+
Low-level utilities, logging, and shared constants for the Agoras ecosystem.
|
|
38
|
+
|
|
39
|
+
Installation
|
|
40
|
+
------------
|
|
41
|
+
|
|
42
|
+
.. code-block:: bash
|
|
43
|
+
|
|
44
|
+
pip install agoras-common
|
|
45
|
+
|
|
46
|
+
Contents
|
|
47
|
+
--------
|
|
48
|
+
|
|
49
|
+
- **Version Info**: ``__version__``, ``__author__``, ``__email__``, ``__url__``, ``__description__``
|
|
50
|
+
- **Utilities**: Helper functions for URL manipulation, metadata parsing
|
|
51
|
+
- **Logger**: Centralized logging configuration
|
|
52
|
+
|
|
53
|
+
Usage
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
.. code-block:: python
|
|
57
|
+
|
|
58
|
+
from agoras.common import __version__, logger, add_url_timestamp, parse_metatags
|
|
59
|
+
|
|
60
|
+
# Version info
|
|
61
|
+
print(f"Agoras version: {__version__}")
|
|
62
|
+
|
|
63
|
+
# Logger
|
|
64
|
+
logger.start()
|
|
65
|
+
logger.loglevel('INFO')
|
|
66
|
+
logger.info("Hello from Agoras!")
|
|
67
|
+
|
|
68
|
+
# URL utilities
|
|
69
|
+
timestamped_url = add_url_timestamp('https://example.com', '20260110')
|
|
70
|
+
print(timestamped_url) # https://example.com?t=20260110
|
|
71
|
+
|
|
72
|
+
# Metatag parsing
|
|
73
|
+
metatags = parse_metatags('https://example.com')
|
|
74
|
+
print(metatags['title'], metatags['image'])
|
|
75
|
+
|
|
76
|
+
Dependencies
|
|
77
|
+
------------
|
|
78
|
+
|
|
79
|
+
None (pure Python utilities)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
agoras-common
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
Low-level utilities, logging, and shared constants for the Agoras ecosystem.
|
|
5
|
+
|
|
6
|
+
Installation
|
|
7
|
+
------------
|
|
8
|
+
|
|
9
|
+
.. code-block:: bash
|
|
10
|
+
|
|
11
|
+
pip install agoras-common
|
|
12
|
+
|
|
13
|
+
Contents
|
|
14
|
+
--------
|
|
15
|
+
|
|
16
|
+
- **Version Info**: ``__version__``, ``__author__``, ``__email__``, ``__url__``, ``__description__``
|
|
17
|
+
- **Utilities**: Helper functions for URL manipulation, metadata parsing
|
|
18
|
+
- **Logger**: Centralized logging configuration
|
|
19
|
+
|
|
20
|
+
Usage
|
|
21
|
+
-----
|
|
22
|
+
|
|
23
|
+
.. code-block:: python
|
|
24
|
+
|
|
25
|
+
from agoras.common import __version__, logger, add_url_timestamp, parse_metatags
|
|
26
|
+
|
|
27
|
+
# Version info
|
|
28
|
+
print(f"Agoras version: {__version__}")
|
|
29
|
+
|
|
30
|
+
# Logger
|
|
31
|
+
logger.start()
|
|
32
|
+
logger.loglevel('INFO')
|
|
33
|
+
logger.info("Hello from Agoras!")
|
|
34
|
+
|
|
35
|
+
# URL utilities
|
|
36
|
+
timestamped_url = add_url_timestamp('https://example.com', '20260110')
|
|
37
|
+
print(timestamped_url) # https://example.com?t=20260110
|
|
38
|
+
|
|
39
|
+
# Metatag parsing
|
|
40
|
+
metatags = parse_metatags('https://example.com')
|
|
41
|
+
print(metatags['title'], metatags['image'])
|
|
42
|
+
|
|
43
|
+
Dependencies
|
|
44
|
+
------------
|
|
45
|
+
|
|
46
|
+
None (pure Python utilities)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
5
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
6
|
+
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
# You should have received a copy of the GNU General Public License
|
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
from setuptools import find_namespace_packages, setup
|
|
21
|
+
|
|
22
|
+
setup(
|
|
23
|
+
name='agoras-common',
|
|
24
|
+
version='2.0.0',
|
|
25
|
+
author='Luis Alejandro Martínez Faneyth',
|
|
26
|
+
author_email='luis@luisalejandro.org',
|
|
27
|
+
url='https://github.com/LuisAlejandro/agoras',
|
|
28
|
+
description='Common utilities and logging for Agoras',
|
|
29
|
+
long_description=open('README.rst').read(),
|
|
30
|
+
long_description_content_type='text/x-rst',
|
|
31
|
+
packages=find_namespace_packages(where='src'),
|
|
32
|
+
package_dir={'': 'src'},
|
|
33
|
+
python_requires='>=3.10',
|
|
34
|
+
install_requires=[
|
|
35
|
+
'requests==2.33.1',
|
|
36
|
+
'beautifulsoup4==4.14.3',
|
|
37
|
+
],
|
|
38
|
+
classifiers=[
|
|
39
|
+
'Development Status :: 5 - Production/Stable',
|
|
40
|
+
'Intended Audience :: Developers',
|
|
41
|
+
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
|
42
|
+
'Natural Language :: English',
|
|
43
|
+
'Programming Language :: Python :: 3',
|
|
44
|
+
'Programming Language :: Python :: 3.10',
|
|
45
|
+
'Programming Language :: Python :: 3.11',
|
|
46
|
+
'Programming Language :: Python :: 3.12',
|
|
47
|
+
'Programming Language :: Python :: 3.13',
|
|
48
|
+
'Programming Language :: Python :: 3.14',
|
|
49
|
+
],
|
|
50
|
+
keywords=['utilities', 'logging', 'social networks'],
|
|
51
|
+
zip_safe=False,
|
|
52
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
"""
|
|
19
|
+
agoras.common
|
|
20
|
+
=============
|
|
21
|
+
|
|
22
|
+
Common utilities, logging, and shared constants for Agoras.
|
|
23
|
+
|
|
24
|
+
This package provides low-level utilities used throughout the Agoras ecosystem:
|
|
25
|
+
- Version and metadata information
|
|
26
|
+
- Logging infrastructure
|
|
27
|
+
- URL manipulation utilities
|
|
28
|
+
- Web scraping utilities
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .logger import ControlableLogger, logger
|
|
32
|
+
from .utils import add_url_timestamp, parse_metatags
|
|
33
|
+
from .version import __author__, __description__, __email__, __url__, __version__
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'__version__',
|
|
37
|
+
'__author__',
|
|
38
|
+
'__email__',
|
|
39
|
+
'__url__',
|
|
40
|
+
'__description__',
|
|
41
|
+
'logger',
|
|
42
|
+
'ControlableLogger',
|
|
43
|
+
'add_url_timestamp',
|
|
44
|
+
'parse_metatags',
|
|
45
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
"""
|
|
19
|
+
``agoras.core.logger`` is the global application logging module.
|
|
20
|
+
|
|
21
|
+
All modules use the same global logging object. No messages will be emitted
|
|
22
|
+
until the logger is started.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from typing import cast
|
|
28
|
+
|
|
29
|
+
levelNames = {
|
|
30
|
+
'CRITICAL': 50,
|
|
31
|
+
'ERROR': 40,
|
|
32
|
+
'WARN': 30,
|
|
33
|
+
'WARNING': 30,
|
|
34
|
+
'INFO': 20,
|
|
35
|
+
'DEBUG': 10,
|
|
36
|
+
'NOTSET': 0,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ControlableLogger(logging.Logger):
|
|
41
|
+
"""
|
|
42
|
+
This class represents a logger object that can be started and stopped.
|
|
43
|
+
|
|
44
|
+
It has a start method which allows you to specify a logging level.
|
|
45
|
+
The stop method halts output.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, name=''):
|
|
49
|
+
"""
|
|
50
|
+
Initialize this ``ControlableLogger``.
|
|
51
|
+
|
|
52
|
+
The name defaults to the application name. Loggers with the same name
|
|
53
|
+
refer to the same underlying object. Names are hierarchical, e.g.
|
|
54
|
+
'parent.child' defines a logger that is a descendant of 'parent'.
|
|
55
|
+
|
|
56
|
+
:param name: a string containing the logger name.
|
|
57
|
+
:return: a ``ControlableLogger`` instance.
|
|
58
|
+
|
|
59
|
+
.. versionadded:: 0.1.0
|
|
60
|
+
"""
|
|
61
|
+
# Initializing parent class
|
|
62
|
+
super(ControlableLogger, self).__init__(name)
|
|
63
|
+
|
|
64
|
+
self.parent = logging.root
|
|
65
|
+
|
|
66
|
+
#: Attribute ``disabled`` (boolean): Stores the current status of the
|
|
67
|
+
#: logger.
|
|
68
|
+
self.disabled = True
|
|
69
|
+
self.propagate = False
|
|
70
|
+
|
|
71
|
+
#: Attribute ``formatstring`` (string): Stores the string that
|
|
72
|
+
#: will be used to format the logger output.
|
|
73
|
+
self.formatstring = '[%(levelname)s] %(message)s'
|
|
74
|
+
|
|
75
|
+
def start(self, filename=None):
|
|
76
|
+
"""
|
|
77
|
+
Start logging with this logger.
|
|
78
|
+
|
|
79
|
+
Until the logger is started, no messages will be emitted. This applies
|
|
80
|
+
to all loggers with the same name and any child loggers.
|
|
81
|
+
|
|
82
|
+
.. versionadded:: 0.1.0
|
|
83
|
+
"""
|
|
84
|
+
if self.disabled:
|
|
85
|
+
sh = logging.StreamHandler(sys.stdout)
|
|
86
|
+
sh.setFormatter(logging.Formatter(self.formatstring))
|
|
87
|
+
self.addHandler(sh)
|
|
88
|
+
if filename:
|
|
89
|
+
fh = logging.FileHandler(filename, mode='w')
|
|
90
|
+
fh.setFormatter(logging.Formatter(self.formatstring))
|
|
91
|
+
self.addHandler(fh)
|
|
92
|
+
self.disabled = False
|
|
93
|
+
|
|
94
|
+
def stop(self):
|
|
95
|
+
"""
|
|
96
|
+
Stop logging with this logger.
|
|
97
|
+
|
|
98
|
+
Remove available handlers and set disabled attribute to ``True``.
|
|
99
|
+
|
|
100
|
+
.. versionadded:: 0.1.0
|
|
101
|
+
"""
|
|
102
|
+
if not self.disabled:
|
|
103
|
+
for h in list(self.handlers):
|
|
104
|
+
self.removeHandler(h)
|
|
105
|
+
self.disabled = True
|
|
106
|
+
|
|
107
|
+
def loglevel(self, level='INFO'):
|
|
108
|
+
"""
|
|
109
|
+
Set the log level for this logger.
|
|
110
|
+
|
|
111
|
+
Messages less than the given priority level will be ignored. The
|
|
112
|
+
default level is 'INFO', which conforms to the *nix convention that
|
|
113
|
+
a successful run should produce no diagnostic output. Available levels
|
|
114
|
+
and their suggested meanings:
|
|
115
|
+
|
|
116
|
+
* ``NOTSET``: all messages are processed.
|
|
117
|
+
* ``DEBUG``: output useful for developers.
|
|
118
|
+
* ``INFO``: trace normal program flow, especially external
|
|
119
|
+
interactions.
|
|
120
|
+
* ``WARNING``: an abnormal condition was detected that might need
|
|
121
|
+
attention.
|
|
122
|
+
* ``ERROR``: an error was detected but execution continued.
|
|
123
|
+
* ``CRITICAL``: an error was detected and execution was halted.
|
|
124
|
+
|
|
125
|
+
:param level: a string containing the desired logging level.
|
|
126
|
+
|
|
127
|
+
.. versionadded:: 0.1.0
|
|
128
|
+
"""
|
|
129
|
+
if not self.disabled:
|
|
130
|
+
self.setLevel(levelNames[level])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
logging.setLoggerClass(ControlableLogger)
|
|
134
|
+
logger = cast(ControlableLogger, logging.getLogger(__name__.split('.')[0]))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
agoras.common.utils
|
|
22
|
+
===================
|
|
23
|
+
|
|
24
|
+
This module contains common and low level functions to all modules in agoras.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
from bs4 import BeautifulSoup
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_url_timestamp(url, timestamp):
|
|
36
|
+
parsed = urlparse(url)
|
|
37
|
+
query = dict(parse_qs(str(parsed.query)))
|
|
38
|
+
query['t'] = timestamp
|
|
39
|
+
parsed = parsed._replace(query=urlencode(query))
|
|
40
|
+
return parsed.geturl()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def metatag(tag):
|
|
44
|
+
return tag.name == "meta" \
|
|
45
|
+
and tag.has_attr("content") \
|
|
46
|
+
and (tag.has_attr("property") or tag.has_attr("name"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_metatags(url, search):
|
|
50
|
+
found = {}
|
|
51
|
+
|
|
52
|
+
response = requests.get(url, timeout=20)
|
|
53
|
+
|
|
54
|
+
if response.status_code != 200:
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
soup = BeautifulSoup(response.content, 'html.parser')
|
|
58
|
+
|
|
59
|
+
for target in search:
|
|
60
|
+
found_meta_tag = soup.find_all(metatag)
|
|
61
|
+
|
|
62
|
+
if not found_meta_tag:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
for meta_tag in found_meta_tag:
|
|
66
|
+
|
|
67
|
+
prop = meta_tag.get("property", "")
|
|
68
|
+
name = meta_tag.get("name", "")
|
|
69
|
+
|
|
70
|
+
if prop == target or name == target:
|
|
71
|
+
found[target] = meta_tag.get("content", "")
|
|
72
|
+
|
|
73
|
+
return found
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_metatags(url):
|
|
77
|
+
|
|
78
|
+
KNOWN_TAGS = [
|
|
79
|
+
"og:title",
|
|
80
|
+
"og:image",
|
|
81
|
+
"og:description",
|
|
82
|
+
"twitter:title",
|
|
83
|
+
"twitter:image",
|
|
84
|
+
"twitter:description",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
data = find_metatags(url, KNOWN_TAGS)
|
|
89
|
+
except Exception:
|
|
90
|
+
data = {}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"title": data.get("og:title", data.get("twitter:title", "")),
|
|
94
|
+
"image": data.get("og:image", data.get("twitter:image", "")),
|
|
95
|
+
"description": data.get("og:description", data.get("twitter:description", "")),
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
"""
|
|
19
|
+
agoras.common.version
|
|
20
|
+
=====================
|
|
21
|
+
|
|
22
|
+
Version and metadata information for Agoras.
|
|
23
|
+
|
|
24
|
+
This module serves as the single source of truth for version info
|
|
25
|
+
across all Agoras packages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__author__ = 'Luis Alejandro Martínez Faneyth'
|
|
29
|
+
__email__ = 'luis@luisalejandro.org'
|
|
30
|
+
__version__ = '2.0.0'
|
|
31
|
+
__url__ = 'https://github.com/LuisAlejandro/agoras'
|
|
32
|
+
__description__ = (
|
|
33
|
+
'A command line python utility to manage your social'
|
|
34
|
+
' networks (Twitter, Facebook, Instagram, LinkedIn, Discord, '
|
|
35
|
+
'YouTube, TikTok, Telegram, Threads, WhatsApp, and X)'
|
|
36
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agoras-common
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Common utilities and logging for Agoras
|
|
5
|
+
Home-page: https://github.com/LuisAlejandro/agoras
|
|
6
|
+
Author: Luis Alejandro Martínez Faneyth
|
|
7
|
+
Author-email: luis@luisalejandro.org
|
|
8
|
+
Keywords: utilities,logging,social networks
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
21
|
+
Requires-Dist: requests==2.33.1
|
|
22
|
+
Requires-Dist: beautifulsoup4==4.14.3
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
agoras-common
|
|
35
|
+
=============
|
|
36
|
+
|
|
37
|
+
Low-level utilities, logging, and shared constants for the Agoras ecosystem.
|
|
38
|
+
|
|
39
|
+
Installation
|
|
40
|
+
------------
|
|
41
|
+
|
|
42
|
+
.. code-block:: bash
|
|
43
|
+
|
|
44
|
+
pip install agoras-common
|
|
45
|
+
|
|
46
|
+
Contents
|
|
47
|
+
--------
|
|
48
|
+
|
|
49
|
+
- **Version Info**: ``__version__``, ``__author__``, ``__email__``, ``__url__``, ``__description__``
|
|
50
|
+
- **Utilities**: Helper functions for URL manipulation, metadata parsing
|
|
51
|
+
- **Logger**: Centralized logging configuration
|
|
52
|
+
|
|
53
|
+
Usage
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
.. code-block:: python
|
|
57
|
+
|
|
58
|
+
from agoras.common import __version__, logger, add_url_timestamp, parse_metatags
|
|
59
|
+
|
|
60
|
+
# Version info
|
|
61
|
+
print(f"Agoras version: {__version__}")
|
|
62
|
+
|
|
63
|
+
# Logger
|
|
64
|
+
logger.start()
|
|
65
|
+
logger.loglevel('INFO')
|
|
66
|
+
logger.info("Hello from Agoras!")
|
|
67
|
+
|
|
68
|
+
# URL utilities
|
|
69
|
+
timestamped_url = add_url_timestamp('https://example.com', '20260110')
|
|
70
|
+
print(timestamped_url) # https://example.com?t=20260110
|
|
71
|
+
|
|
72
|
+
# Metatag parsing
|
|
73
|
+
metatags = parse_metatags('https://example.com')
|
|
74
|
+
print(metatags['title'], metatags['image'])
|
|
75
|
+
|
|
76
|
+
Dependencies
|
|
77
|
+
------------
|
|
78
|
+
|
|
79
|
+
None (pure Python utilities)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.rst
|
|
2
|
+
setup.py
|
|
3
|
+
src/agoras/__init__.py
|
|
4
|
+
src/agoras/common/__init__.py
|
|
5
|
+
src/agoras/common/logger.py
|
|
6
|
+
src/agoras/common/utils.py
|
|
7
|
+
src/agoras/common/version.py
|
|
8
|
+
src/agoras_common.egg-info/PKG-INFO
|
|
9
|
+
src/agoras_common.egg-info/SOURCES.txt
|
|
10
|
+
src/agoras_common.egg-info/dependency_links.txt
|
|
11
|
+
src/agoras_common.egg-info/not-zip-safe
|
|
12
|
+
src/agoras_common.egg-info/requires.txt
|
|
13
|
+
src/agoras_common.egg-info/top_level.txt
|
|
14
|
+
tests/test_logger.py
|
|
15
|
+
tests/test_utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agoras
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
import doctest
|
|
20
|
+
import io
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
import unittest
|
|
26
|
+
|
|
27
|
+
from agoras.common.logger import levelNames, logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestLogger(unittest.TestCase):
|
|
31
|
+
|
|
32
|
+
def setUp(self):
|
|
33
|
+
# Ensure logger is stopped before each test
|
|
34
|
+
if not logger.disabled:
|
|
35
|
+
logger.stop()
|
|
36
|
+
|
|
37
|
+
def tearDown(self):
|
|
38
|
+
# Clean up after each test
|
|
39
|
+
if not logger.disabled:
|
|
40
|
+
logger.stop()
|
|
41
|
+
|
|
42
|
+
def test_01_default_level(self):
|
|
43
|
+
"""Test logger default state."""
|
|
44
|
+
# Logger should be disabled initially
|
|
45
|
+
self.assertTrue(logger.disabled)
|
|
46
|
+
|
|
47
|
+
def test_start_without_filename(self):
|
|
48
|
+
"""Test starting logger without filename parameter."""
|
|
49
|
+
logger.start()
|
|
50
|
+
|
|
51
|
+
# Logger should be enabled
|
|
52
|
+
self.assertFalse(logger.disabled)
|
|
53
|
+
|
|
54
|
+
# Should have exactly one handler (StreamHandler)
|
|
55
|
+
self.assertEqual(len(logger.handlers), 1)
|
|
56
|
+
self.assertIsInstance(logger.handlers[0], logging.StreamHandler)
|
|
57
|
+
|
|
58
|
+
def test_start_with_filename(self):
|
|
59
|
+
"""Test starting logger with filename parameter."""
|
|
60
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
|
61
|
+
temp_filename = f.name
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
logger.start(filename=temp_filename)
|
|
65
|
+
|
|
66
|
+
# Logger should be enabled
|
|
67
|
+
self.assertFalse(logger.disabled)
|
|
68
|
+
|
|
69
|
+
# Should have two handlers (StreamHandler + FileHandler)
|
|
70
|
+
self.assertEqual(len(logger.handlers), 2)
|
|
71
|
+
|
|
72
|
+
# Check handler types
|
|
73
|
+
handler_types = [type(h).__name__ for h in logger.handlers]
|
|
74
|
+
self.assertIn('StreamHandler', handler_types)
|
|
75
|
+
self.assertIn('FileHandler', handler_types)
|
|
76
|
+
|
|
77
|
+
logger.stop()
|
|
78
|
+
finally:
|
|
79
|
+
# Clean up temp file
|
|
80
|
+
if os.path.exists(temp_filename):
|
|
81
|
+
os.remove(temp_filename)
|
|
82
|
+
|
|
83
|
+
def test_start_when_already_started(self):
|
|
84
|
+
"""Test that starting an already started logger doesn't add duplicate handlers."""
|
|
85
|
+
logger.start()
|
|
86
|
+
initial_handler_count = len(logger.handlers)
|
|
87
|
+
|
|
88
|
+
# Try to start again
|
|
89
|
+
logger.start()
|
|
90
|
+
|
|
91
|
+
# Handler count should not increase
|
|
92
|
+
self.assertEqual(len(logger.handlers), initial_handler_count)
|
|
93
|
+
|
|
94
|
+
def test_stop_after_start(self):
|
|
95
|
+
"""Test stopping logger after starting."""
|
|
96
|
+
logger.start()
|
|
97
|
+
self.assertFalse(logger.disabled)
|
|
98
|
+
self.assertGreater(len(logger.handlers), 0)
|
|
99
|
+
|
|
100
|
+
logger.stop()
|
|
101
|
+
|
|
102
|
+
# Logger should be disabled
|
|
103
|
+
self.assertTrue(logger.disabled)
|
|
104
|
+
|
|
105
|
+
# All handlers should be removed
|
|
106
|
+
self.assertEqual(len(logger.handlers), 0)
|
|
107
|
+
|
|
108
|
+
def test_stop_when_already_stopped(self):
|
|
109
|
+
"""Test stopping an already stopped logger."""
|
|
110
|
+
# Ensure logger is stopped
|
|
111
|
+
if not logger.disabled:
|
|
112
|
+
logger.stop()
|
|
113
|
+
|
|
114
|
+
# Should not raise error
|
|
115
|
+
logger.stop()
|
|
116
|
+
|
|
117
|
+
# Should still be disabled
|
|
118
|
+
self.assertTrue(logger.disabled)
|
|
119
|
+
|
|
120
|
+
def test_stop_clears_all_handlers(self):
|
|
121
|
+
"""Test that stop clears all handlers including file handler."""
|
|
122
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
|
123
|
+
temp_filename = f.name
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
logger.start(filename=temp_filename)
|
|
127
|
+
self.assertEqual(len(logger.handlers), 2)
|
|
128
|
+
|
|
129
|
+
logger.stop()
|
|
130
|
+
|
|
131
|
+
# All handlers should be cleared
|
|
132
|
+
self.assertEqual(len(logger.handlers), 0)
|
|
133
|
+
finally:
|
|
134
|
+
if os.path.exists(temp_filename):
|
|
135
|
+
os.remove(temp_filename)
|
|
136
|
+
|
|
137
|
+
def test_loglevel_sets_debug(self):
|
|
138
|
+
"""Test setting log level to DEBUG."""
|
|
139
|
+
logger.start()
|
|
140
|
+
logger.loglevel('DEBUG')
|
|
141
|
+
|
|
142
|
+
self.assertEqual(logger.level, 10)
|
|
143
|
+
|
|
144
|
+
logger.stop()
|
|
145
|
+
|
|
146
|
+
def test_loglevel_sets_info(self):
|
|
147
|
+
"""Test setting log level to INFO."""
|
|
148
|
+
logger.start()
|
|
149
|
+
logger.loglevel('INFO')
|
|
150
|
+
|
|
151
|
+
self.assertEqual(logger.level, 20)
|
|
152
|
+
|
|
153
|
+
logger.stop()
|
|
154
|
+
|
|
155
|
+
def test_loglevel_sets_warning(self):
|
|
156
|
+
"""Test setting log level to WARNING."""
|
|
157
|
+
logger.start()
|
|
158
|
+
logger.loglevel('WARNING')
|
|
159
|
+
|
|
160
|
+
self.assertEqual(logger.level, 30)
|
|
161
|
+
|
|
162
|
+
logger.stop()
|
|
163
|
+
|
|
164
|
+
def test_loglevel_sets_error(self):
|
|
165
|
+
"""Test setting log level to ERROR."""
|
|
166
|
+
logger.start()
|
|
167
|
+
logger.loglevel('ERROR')
|
|
168
|
+
|
|
169
|
+
self.assertEqual(logger.level, 40)
|
|
170
|
+
|
|
171
|
+
logger.stop()
|
|
172
|
+
|
|
173
|
+
def test_loglevel_sets_critical(self):
|
|
174
|
+
"""Test setting log level to CRITICAL."""
|
|
175
|
+
logger.start()
|
|
176
|
+
logger.loglevel('CRITICAL')
|
|
177
|
+
|
|
178
|
+
self.assertEqual(logger.level, 50)
|
|
179
|
+
|
|
180
|
+
logger.stop()
|
|
181
|
+
|
|
182
|
+
def test_loglevel_sets_notset(self):
|
|
183
|
+
"""Test setting log level to NOTSET."""
|
|
184
|
+
logger.start()
|
|
185
|
+
logger.loglevel('NOTSET')
|
|
186
|
+
|
|
187
|
+
self.assertEqual(logger.level, 0)
|
|
188
|
+
|
|
189
|
+
logger.stop()
|
|
190
|
+
|
|
191
|
+
def test_loglevel_on_disabled_logger(self):
|
|
192
|
+
"""Test that loglevel has no effect on disabled logger."""
|
|
193
|
+
# Ensure logger is disabled
|
|
194
|
+
if not logger.disabled:
|
|
195
|
+
logger.stop()
|
|
196
|
+
|
|
197
|
+
initial_level = logger.level
|
|
198
|
+
|
|
199
|
+
# Try to set level on disabled logger
|
|
200
|
+
logger.loglevel('DEBUG')
|
|
201
|
+
|
|
202
|
+
# Level should not change
|
|
203
|
+
self.assertEqual(logger.level, initial_level)
|
|
204
|
+
|
|
205
|
+
def test_logger_format_string(self):
|
|
206
|
+
"""Test that logger uses correct format string."""
|
|
207
|
+
self.assertEqual(logger.formatstring, '[%(levelname)s] %(message)s')
|
|
208
|
+
|
|
209
|
+
def test_logger_actually_logs_messages(self):
|
|
210
|
+
"""Test that logger actually outputs log messages."""
|
|
211
|
+
# Create a string buffer to capture output
|
|
212
|
+
string_buffer = io.StringIO()
|
|
213
|
+
|
|
214
|
+
# Create a handler that writes to our buffer
|
|
215
|
+
handler = logging.StreamHandler(string_buffer)
|
|
216
|
+
handler.setFormatter(logging.Formatter(logger.formatstring))
|
|
217
|
+
|
|
218
|
+
# Manually add handler (simulating start behavior)
|
|
219
|
+
logger.addHandler(handler)
|
|
220
|
+
logger.disabled = False
|
|
221
|
+
logger.setLevel(logging.INFO)
|
|
222
|
+
|
|
223
|
+
# Log a message
|
|
224
|
+
logger.info('Test message')
|
|
225
|
+
|
|
226
|
+
# Get output
|
|
227
|
+
output = string_buffer.getvalue()
|
|
228
|
+
|
|
229
|
+
# Verify message appears with correct format
|
|
230
|
+
self.assertIn('[INFO] Test message', output)
|
|
231
|
+
|
|
232
|
+
# Clean up
|
|
233
|
+
logger.removeHandler(handler)
|
|
234
|
+
logger.disabled = True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def load_tests(loader, tests, pattern):
|
|
238
|
+
tests.addTests(doctest.DocTestSuite('agoras.common.logger'))
|
|
239
|
+
return tests
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == '__main__':
|
|
243
|
+
sys.exit(unittest.main())
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Please refer to AUTHORS.rst for a complete list of Copyright holders.
|
|
4
|
+
# Copyright (C) 2022-2026, Agoras Developers.
|
|
5
|
+
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
import doctest
|
|
20
|
+
import sys
|
|
21
|
+
import unittest
|
|
22
|
+
from unittest.mock import Mock, patch
|
|
23
|
+
|
|
24
|
+
from bs4 import BeautifulSoup
|
|
25
|
+
|
|
26
|
+
from agoras.common.utils import add_url_timestamp, find_metatags, metatag, parse_metatags
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestAddUrlTimestamp(unittest.TestCase):
|
|
30
|
+
"""Tests for add_url_timestamp function."""
|
|
31
|
+
|
|
32
|
+
def test_add_timestamp_to_url_without_query(self):
|
|
33
|
+
"""Test adding timestamp to URL without query parameters."""
|
|
34
|
+
url = "https://example.com"
|
|
35
|
+
timestamp = 12345
|
|
36
|
+
result = add_url_timestamp(url, timestamp)
|
|
37
|
+
self.assertIn("t=12345", result)
|
|
38
|
+
self.assertTrue(result.startswith("https://example.com?"))
|
|
39
|
+
|
|
40
|
+
def test_add_timestamp_to_url_with_existing_query(self):
|
|
41
|
+
"""Test adding timestamp to URL with existing query parameters."""
|
|
42
|
+
url = "https://example.com?foo=bar"
|
|
43
|
+
timestamp = 67890
|
|
44
|
+
result = add_url_timestamp(url, timestamp)
|
|
45
|
+
# parse_qs returns lists, so foo will be encoded as ['bar']
|
|
46
|
+
self.assertIn("foo=", result)
|
|
47
|
+
self.assertIn("t=67890", result)
|
|
48
|
+
self.assertTrue(result.startswith("https://example.com?"))
|
|
49
|
+
|
|
50
|
+
def test_replace_existing_timestamp(self):
|
|
51
|
+
"""Test replacing existing timestamp parameter."""
|
|
52
|
+
url = "https://example.com?t=111&foo=bar"
|
|
53
|
+
timestamp = 999
|
|
54
|
+
result = add_url_timestamp(url, timestamp)
|
|
55
|
+
self.assertIn("t=999", result)
|
|
56
|
+
self.assertIn("foo=", result)
|
|
57
|
+
self.assertNotIn("t=111", result)
|
|
58
|
+
|
|
59
|
+
def test_url_with_fragment(self):
|
|
60
|
+
"""Test URL with fragment."""
|
|
61
|
+
url = "https://example.com#section"
|
|
62
|
+
timestamp = 55555
|
|
63
|
+
result = add_url_timestamp(url, timestamp)
|
|
64
|
+
self.assertIn("t=55555", result)
|
|
65
|
+
self.assertIn("#section", result)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestMetatag(unittest.TestCase):
|
|
69
|
+
"""Tests for metatag function."""
|
|
70
|
+
|
|
71
|
+
def test_valid_meta_tag_with_property(self):
|
|
72
|
+
"""Test with valid meta tag that has property attribute."""
|
|
73
|
+
html = '<meta property="og:title" content="Test">'
|
|
74
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
75
|
+
tag = soup.find('meta')
|
|
76
|
+
result = metatag(tag)
|
|
77
|
+
self.assertTrue(result)
|
|
78
|
+
|
|
79
|
+
def test_valid_meta_tag_with_name(self):
|
|
80
|
+
"""Test with valid meta tag that has name attribute."""
|
|
81
|
+
html = '<meta name="description" content="Test">'
|
|
82
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
83
|
+
tag = soup.find('meta')
|
|
84
|
+
result = metatag(tag)
|
|
85
|
+
self.assertTrue(result)
|
|
86
|
+
|
|
87
|
+
def test_invalid_tag_not_meta(self):
|
|
88
|
+
"""Test with invalid tag (not meta)."""
|
|
89
|
+
html = '<div content="Test">Text</div>'
|
|
90
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
91
|
+
tag = soup.find('div')
|
|
92
|
+
result = metatag(tag)
|
|
93
|
+
self.assertFalse(result)
|
|
94
|
+
|
|
95
|
+
def test_meta_tag_without_content(self):
|
|
96
|
+
"""Test with meta tag without content attribute."""
|
|
97
|
+
html = '<meta property="og:title">'
|
|
98
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
99
|
+
tag = soup.find('meta')
|
|
100
|
+
result = metatag(tag)
|
|
101
|
+
self.assertFalse(result)
|
|
102
|
+
|
|
103
|
+
def test_meta_tag_without_property_or_name(self):
|
|
104
|
+
"""Test with meta tag without property or name attribute."""
|
|
105
|
+
html = '<meta content="Test">'
|
|
106
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
107
|
+
tag = soup.find('meta')
|
|
108
|
+
result = metatag(tag)
|
|
109
|
+
self.assertFalse(result)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestFindMetatags(unittest.TestCase):
|
|
113
|
+
"""Tests for find_metatags function."""
|
|
114
|
+
|
|
115
|
+
@patch('agoras.common.utils.requests.get')
|
|
116
|
+
def test_successful_meta_tag_extraction(self, mock_get):
|
|
117
|
+
"""Test successful meta tag extraction."""
|
|
118
|
+
mock_response = Mock()
|
|
119
|
+
mock_response.status_code = 200
|
|
120
|
+
mock_response.content = b'<html><meta property="og:title" content="My Title"></html>'
|
|
121
|
+
mock_get.return_value = mock_response
|
|
122
|
+
|
|
123
|
+
result = find_metatags('https://example.com', ['og:title'])
|
|
124
|
+
self.assertEqual(result, {'og:title': 'My Title'})
|
|
125
|
+
|
|
126
|
+
@patch('agoras.common.utils.requests.get')
|
|
127
|
+
def test_multiple_meta_tags(self, mock_get):
|
|
128
|
+
"""Test extraction of multiple meta tags."""
|
|
129
|
+
html = '''<html>
|
|
130
|
+
<meta property="og:title" content="Title">
|
|
131
|
+
<meta property="og:image" content="image.jpg">
|
|
132
|
+
<meta name="twitter:description" content="Description">
|
|
133
|
+
</html>'''
|
|
134
|
+
mock_response = Mock()
|
|
135
|
+
mock_response.status_code = 200
|
|
136
|
+
mock_response.content = html.encode()
|
|
137
|
+
mock_get.return_value = mock_response
|
|
138
|
+
|
|
139
|
+
result = find_metatags('https://example.com',
|
|
140
|
+
['og:title', 'og:image', 'twitter:description'])
|
|
141
|
+
self.assertEqual(result['og:title'], 'Title')
|
|
142
|
+
self.assertEqual(result['og:image'], 'image.jpg')
|
|
143
|
+
self.assertEqual(result['twitter:description'], 'Description')
|
|
144
|
+
|
|
145
|
+
@patch('agoras.common.utils.requests.get')
|
|
146
|
+
def test_non_200_status_code(self, mock_get):
|
|
147
|
+
"""Test with non-200 status code."""
|
|
148
|
+
mock_response = Mock()
|
|
149
|
+
mock_response.status_code = 404
|
|
150
|
+
mock_get.return_value = mock_response
|
|
151
|
+
|
|
152
|
+
result = find_metatags('https://example.com', ['og:title'])
|
|
153
|
+
self.assertEqual(result, {})
|
|
154
|
+
|
|
155
|
+
@patch('agoras.common.utils.requests.get')
|
|
156
|
+
def test_html_without_matching_meta_tags(self, mock_get):
|
|
157
|
+
"""Test HTML without matching meta tags."""
|
|
158
|
+
mock_response = Mock()
|
|
159
|
+
mock_response.status_code = 200
|
|
160
|
+
mock_response.content = b'<html><p>No meta tags here</p></html>'
|
|
161
|
+
mock_get.return_value = mock_response
|
|
162
|
+
|
|
163
|
+
result = find_metatags('https://example.com', ['og:title'])
|
|
164
|
+
self.assertEqual(result, {})
|
|
165
|
+
|
|
166
|
+
@patch('agoras.common.utils.requests.get')
|
|
167
|
+
def test_meta_tags_with_name_attribute(self, mock_get):
|
|
168
|
+
"""Test meta tags with name attribute."""
|
|
169
|
+
html = '<html><meta name="twitter:title" content="Twitter Title"></html>'
|
|
170
|
+
mock_response = Mock()
|
|
171
|
+
mock_response.status_code = 200
|
|
172
|
+
mock_response.content = html.encode()
|
|
173
|
+
mock_get.return_value = mock_response
|
|
174
|
+
|
|
175
|
+
result = find_metatags('https://example.com', ['twitter:title'])
|
|
176
|
+
self.assertEqual(result, {'twitter:title': 'Twitter Title'})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestParseMetatags(unittest.TestCase):
|
|
180
|
+
"""Tests for parse_metatags function."""
|
|
181
|
+
|
|
182
|
+
@patch('agoras.common.utils.find_metatags')
|
|
183
|
+
def test_with_open_graph_tags(self, mock_find):
|
|
184
|
+
"""Test with Open Graph tags."""
|
|
185
|
+
mock_find.return_value = {
|
|
186
|
+
'og:title': 'OG Title',
|
|
187
|
+
'og:image': 'og.jpg',
|
|
188
|
+
'og:description': 'OG Desc'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result = parse_metatags('https://example.com')
|
|
192
|
+
self.assertEqual(result, {
|
|
193
|
+
'title': 'OG Title',
|
|
194
|
+
'image': 'og.jpg',
|
|
195
|
+
'description': 'OG Desc'
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
@patch('agoras.common.utils.find_metatags')
|
|
199
|
+
def test_with_twitter_tags_fallback(self, mock_find):
|
|
200
|
+
"""Test with Twitter tags as fallback."""
|
|
201
|
+
mock_find.return_value = {
|
|
202
|
+
'twitter:title': 'Twitter Title',
|
|
203
|
+
'twitter:image': 'twitter.jpg',
|
|
204
|
+
'twitter:description': 'Twitter Desc'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result = parse_metatags('https://example.com')
|
|
208
|
+
self.assertEqual(result, {
|
|
209
|
+
'title': 'Twitter Title',
|
|
210
|
+
'image': 'twitter.jpg',
|
|
211
|
+
'description': 'Twitter Desc'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
@patch('agoras.common.utils.find_metatags')
|
|
215
|
+
def test_with_mixed_tags(self, mock_find):
|
|
216
|
+
"""Test with mixed OG and Twitter tags."""
|
|
217
|
+
mock_find.return_value = {
|
|
218
|
+
'og:title': 'OG Title',
|
|
219
|
+
'twitter:image': 'twitter.jpg',
|
|
220
|
+
'og:description': 'OG Desc'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
result = parse_metatags('https://example.com')
|
|
224
|
+
self.assertEqual(result['title'], 'OG Title')
|
|
225
|
+
self.assertEqual(result['image'], 'twitter.jpg')
|
|
226
|
+
self.assertEqual(result['description'], 'OG Desc')
|
|
227
|
+
|
|
228
|
+
@patch('agoras.common.utils.find_metatags')
|
|
229
|
+
def test_with_no_tags_found(self, mock_find):
|
|
230
|
+
"""Test with no tags found."""
|
|
231
|
+
mock_find.return_value = {}
|
|
232
|
+
|
|
233
|
+
result = parse_metatags('https://example.com')
|
|
234
|
+
self.assertEqual(result, {
|
|
235
|
+
'title': '',
|
|
236
|
+
'image': '',
|
|
237
|
+
'description': ''
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
@patch('agoras.common.utils.find_metatags')
|
|
241
|
+
def test_exception_handling(self, mock_find):
|
|
242
|
+
"""Test exception handling."""
|
|
243
|
+
mock_find.side_effect = Exception('Network error')
|
|
244
|
+
|
|
245
|
+
result = parse_metatags('https://example.com')
|
|
246
|
+
self.assertEqual(result, {
|
|
247
|
+
'title': '',
|
|
248
|
+
'image': '',
|
|
249
|
+
'description': ''
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def load_tests(loader, tests, pattern):
|
|
254
|
+
tests.addTests(doctest.DocTestSuite('agoras.common.utils'))
|
|
255
|
+
return tests
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == '__main__':
|
|
259
|
+
sys.exit(unittest.main())
|