scitex 2.14.0__py3-none-any.whl → 2.15.2__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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +481 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +23 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +132 -8
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.2.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-07-29 03:10:08 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/scholar/open_url/_ResolverLinkFinder.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
__FILE__ = (
|
|
11
|
+
"./src/scitex/scholar/open_url/_ResolverLinkFinder.py"
|
|
12
|
+
)
|
|
13
|
+
__DIR__ = os.path.dirname(__FILE__)
|
|
14
|
+
# ----------------------------------------
|
|
15
|
+
|
|
16
|
+
"""Robust resolver link finder using a prioritized, multi-layered approach.
|
|
17
|
+
|
|
18
|
+
Priority order:
|
|
19
|
+
1. Link Target (domain matching) - Most reliable
|
|
20
|
+
2. Page Structure (CSS selectors) - Very reliable
|
|
21
|
+
3. Text Patterns - Good fallback
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from typing import List, Optional
|
|
26
|
+
from urllib.parse import urlparse
|
|
27
|
+
|
|
28
|
+
from playwright.async_api import ElementHandle, Page
|
|
29
|
+
|
|
30
|
+
from scitex import logging
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ResolverLinkFinder:
|
|
36
|
+
"""Finds full-text links on resolver pages using multiple strategies."""
|
|
37
|
+
|
|
38
|
+
# DOI prefix to publisher domain mapping
|
|
39
|
+
DOI_TO_DOMAIN = {
|
|
40
|
+
"10.1038": [
|
|
41
|
+
"nature.com",
|
|
42
|
+
"springernature.com",
|
|
43
|
+
], # Nature Publishing Group
|
|
44
|
+
"10.1016": ["sciencedirect.com", "elsevier.com"], # Elsevier
|
|
45
|
+
"10.1002": ["wiley.com", "onlinelibrary.wiley.com"], # Wiley
|
|
46
|
+
"10.1007": ["springer.com", "link.springer.com"], # Springer
|
|
47
|
+
"10.1126": ["science.org", "sciencemag.org"], # Science/AAAS
|
|
48
|
+
"10.1021": ["acs.org", "pubs.acs.org"], # ACS Publications
|
|
49
|
+
"10.1111": [
|
|
50
|
+
"wiley.com",
|
|
51
|
+
"onlinelibrary.wiley.com",
|
|
52
|
+
], # Wiley (alternative)
|
|
53
|
+
"10.1080": ["tandfonline.com"], # Taylor & Francis
|
|
54
|
+
"10.1177": ["sagepub.com", "journals.sagepub.com"], # SAGE
|
|
55
|
+
"10.1093": ["oup.com", "academic.oup.com"], # Oxford
|
|
56
|
+
"10.1109": ["ieee.org", "ieeexplore.ieee.org"], # IEEE
|
|
57
|
+
"10.1371": ["plos.org", "journals.plos.org"], # PLOS
|
|
58
|
+
"10.1073": ["pnas.org"], # PNAS
|
|
59
|
+
"10.1136": ["bmj.com"], # BMJ
|
|
60
|
+
"10.3389": ["frontiersin.org"], # Frontiers
|
|
61
|
+
"10.3390": ["mdpi.com"], # MDPI
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Common resolver page structures
|
|
65
|
+
STRUCTURE_SELECTORS = [
|
|
66
|
+
# SFX (ExLibris) - used by many universities
|
|
67
|
+
"div#fulltext a",
|
|
68
|
+
"div.sfx-fulltext a",
|
|
69
|
+
"div.results-title > a",
|
|
70
|
+
"td.object-cell a",
|
|
71
|
+
".getFullTxt a",
|
|
72
|
+
'div[id*="fulltext"] a',
|
|
73
|
+
'div[class*="fulltext"] a',
|
|
74
|
+
# SFX specific selectors for University of Melbourne
|
|
75
|
+
"a[title*='Wiley Online Library']",
|
|
76
|
+
"a[href*='wiley.com']",
|
|
77
|
+
"a[href*='onlinelibrary.wiley.com']",
|
|
78
|
+
".sfx-target a",
|
|
79
|
+
".target a",
|
|
80
|
+
"td a[href*='wiley']",
|
|
81
|
+
# Primo (ExLibris)
|
|
82
|
+
"prm-full-view-service-container a",
|
|
83
|
+
"span.availability-status-available a",
|
|
84
|
+
# Summon (ProQuest)
|
|
85
|
+
".summon-fulltext-link",
|
|
86
|
+
"a.summon-link",
|
|
87
|
+
# EDS (EBSCO)
|
|
88
|
+
"a.fulltext-link",
|
|
89
|
+
".ft-link a",
|
|
90
|
+
# Generic patterns
|
|
91
|
+
"a.full-text-link",
|
|
92
|
+
"a.fulltext",
|
|
93
|
+
"a#full-text-link",
|
|
94
|
+
".access-link a",
|
|
95
|
+
".available-link a",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Text patterns in priority order
|
|
99
|
+
TEXT_PATTERNS = [
|
|
100
|
+
# Most specific
|
|
101
|
+
"View full text at",
|
|
102
|
+
"Available from Nature",
|
|
103
|
+
"Available from ScienceDirect",
|
|
104
|
+
"Available from Wiley",
|
|
105
|
+
"Available from Wiley Online Library",
|
|
106
|
+
"Full text available from",
|
|
107
|
+
# Common patterns
|
|
108
|
+
"View full text",
|
|
109
|
+
"Full Text from Publisher",
|
|
110
|
+
"Get full text",
|
|
111
|
+
"Access full text",
|
|
112
|
+
"Go to article",
|
|
113
|
+
"Access article",
|
|
114
|
+
# Generic but reliable
|
|
115
|
+
"Full Text",
|
|
116
|
+
"Full text",
|
|
117
|
+
"Article",
|
|
118
|
+
"View",
|
|
119
|
+
"PDF",
|
|
120
|
+
"Download",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def __init__(self):
|
|
124
|
+
self._doi_pattern = re.compile(r"10\.\d{4,}/[-._;()/:\w]+")
|
|
125
|
+
|
|
126
|
+
def get_expected_domains(self, doi: str) -> List[str]:
|
|
127
|
+
"""Get expected publisher domains for a DOI."""
|
|
128
|
+
# Extract DOI prefix
|
|
129
|
+
match = re.match(r"(10\.\d{4,})", doi)
|
|
130
|
+
if not match:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
prefix = match.group(1)
|
|
134
|
+
return self.DOI_TO_DOMAIN.get(prefix, [])
|
|
135
|
+
|
|
136
|
+
async def find_link_async(self, page, doi: str) -> dict:
|
|
137
|
+
"""Find the best full-text link using prioritized strategies."""
|
|
138
|
+
logger.info(f"Finding resolver link for DOI: {doi}")
|
|
139
|
+
|
|
140
|
+
# Strategy 1: Link Target (Most Reliable)
|
|
141
|
+
link_url = await self._find_by_domain_async(page, doi)
|
|
142
|
+
if link_url:
|
|
143
|
+
logger.info("✓ Found link using domain matching (Strategy 1)")
|
|
144
|
+
return {"success": True, "url": link_url, "method": "domain"}
|
|
145
|
+
|
|
146
|
+
# Strategy 2: Page Structure with scoring
|
|
147
|
+
link_url = await self._find_by_structure_async(page, doi)
|
|
148
|
+
if link_url:
|
|
149
|
+
logger.info("✓ Found link using page structure (Strategy 2)")
|
|
150
|
+
return {"success": True, "url": link_url, "method": "structure"}
|
|
151
|
+
|
|
152
|
+
logger.warning("✗ No suitable links found")
|
|
153
|
+
return {"success": False, "url": None, "method": None}
|
|
154
|
+
|
|
155
|
+
async def _find_by_domain_async(self, page: Page, doi: str) -> Optional[str]:
|
|
156
|
+
"""Strategy 1: Find link by expected publisher domain."""
|
|
157
|
+
expected_domains = self.get_expected_domains(doi)
|
|
158
|
+
if not expected_domains:
|
|
159
|
+
logger.debug(f"No known publisher domains for DOI prefix: {doi}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
logger.debug(f"Looking for links to domains: {expected_domains}")
|
|
163
|
+
all_links = await page.query_selector_all("a[href]")
|
|
164
|
+
|
|
165
|
+
for link in all_links:
|
|
166
|
+
href = await link.get_attribute("href")
|
|
167
|
+
if not href:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
parsed = urlparse(href)
|
|
172
|
+
domain = parsed.netloc.lower()
|
|
173
|
+
|
|
174
|
+
for expected in expected_domains:
|
|
175
|
+
if expected in domain:
|
|
176
|
+
text = await link.inner_text() or ""
|
|
177
|
+
logger.info(
|
|
178
|
+
f"Found domain match: {domain} (text: '{text[:50]}')"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not any(
|
|
182
|
+
bad in text.lower()
|
|
183
|
+
for bad in ["abstract", "preview", "summary"]
|
|
184
|
+
):
|
|
185
|
+
return href
|
|
186
|
+
else:
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"Skipping abstract/preview link: {text}"
|
|
189
|
+
)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.debug(f"Error parsing URL {href}: {e}")
|
|
192
|
+
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
async def _find_by_structure_async(self, page, doi: str):
|
|
196
|
+
"""Find link by page structure with publisher prioritization."""
|
|
197
|
+
potential_links = []
|
|
198
|
+
expected_domains = self.get_expected_domains(doi)
|
|
199
|
+
publisher_keywords = [
|
|
200
|
+
domain.split(".")[0] for domain in expected_domains
|
|
201
|
+
]
|
|
202
|
+
aggregator_keywords = ["gale", "proquest", "ebsco", "jstor", "onefile"]
|
|
203
|
+
|
|
204
|
+
# Gather all possible links
|
|
205
|
+
for selector in self.STRUCTURE_SELECTORS:
|
|
206
|
+
try:
|
|
207
|
+
elements = await page.query_selector_all(selector)
|
|
208
|
+
logger.debug(
|
|
209
|
+
f"Found {len(elements)} elements with selector: {selector}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for element in elements:
|
|
213
|
+
if await element.is_visible():
|
|
214
|
+
href = await element.get_attribute("href")
|
|
215
|
+
text = (await element.inner_text() or "").lower()
|
|
216
|
+
|
|
217
|
+
if href and href.strip():
|
|
218
|
+
potential_links.append(
|
|
219
|
+
{"href": href, "text": text, "score": 0}
|
|
220
|
+
)
|
|
221
|
+
except Exception as element_error:
|
|
222
|
+
logger.debug(
|
|
223
|
+
f"Error with selector '{selector}': {element_error}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if not potential_links:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Score the links
|
|
230
|
+
for link in potential_links:
|
|
231
|
+
# Highest score for direct publisher match
|
|
232
|
+
if any(keyword in link["text"] for keyword in publisher_keywords):
|
|
233
|
+
link["score"] = 3
|
|
234
|
+
# High score for generic publisher
|
|
235
|
+
elif "publisher" in link["text"]:
|
|
236
|
+
link["score"] = 2
|
|
237
|
+
# Negative score for aggregators
|
|
238
|
+
elif any(
|
|
239
|
+
keyword in link["text"] for keyword in aggregator_keywords
|
|
240
|
+
):
|
|
241
|
+
link["score"] = -1
|
|
242
|
+
# Default neutral score
|
|
243
|
+
else:
|
|
244
|
+
link["score"] = 0
|
|
245
|
+
|
|
246
|
+
# Sort by score, highest first
|
|
247
|
+
sorted_links = sorted(
|
|
248
|
+
potential_links, key=lambda x: x["score"], reverse=True
|
|
249
|
+
)
|
|
250
|
+
best_link = sorted_links[0]
|
|
251
|
+
|
|
252
|
+
logger.debug(
|
|
253
|
+
f"Found structural match: '{best_link['text'][:50]}' -> {best_link['href']}"
|
|
254
|
+
)
|
|
255
|
+
return best_link["href"]
|
|
256
|
+
|
|
257
|
+
async def _find_by_text_async(self, page: Page) -> Optional[str]:
|
|
258
|
+
"""Strategy 3: Find link by text patterns."""
|
|
259
|
+
for pattern in self.TEXT_PATTERNS:
|
|
260
|
+
try:
|
|
261
|
+
selector = f'a:has-text("{pattern}")'
|
|
262
|
+
link = await page.query_selector(selector)
|
|
263
|
+
if link and await link.is_visible():
|
|
264
|
+
href = await link.get_attribute("href")
|
|
265
|
+
if href and href.strip():
|
|
266
|
+
logger.debug(
|
|
267
|
+
f"Found text match: '{pattern}' -> {href[:100]}"
|
|
268
|
+
)
|
|
269
|
+
return href
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.debug(f"Error with text pattern '{pattern}': {e}")
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
async def click_and_wait_async(self, page: Page, link: ElementHandle) -> bool:
|
|
276
|
+
"""Click link and wait for navigation.
|
|
277
|
+
|
|
278
|
+
Returns True if navigation succeeded.
|
|
279
|
+
"""
|
|
280
|
+
initial_url = page.url
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
# Get link info for logging
|
|
284
|
+
href = await link.get_attribute("href") or ""
|
|
285
|
+
text = await link.inner_text() or ""
|
|
286
|
+
logger.info(f"Clicking link: '{text[:50]}' -> {href[:100]}")
|
|
287
|
+
|
|
288
|
+
# Click and wait for navigation
|
|
289
|
+
await link.click()
|
|
290
|
+
|
|
291
|
+
# Wait for either navigation or network idle
|
|
292
|
+
try:
|
|
293
|
+
await page.wait_for_load_state("networkidle", timeout=30000)
|
|
294
|
+
except:
|
|
295
|
+
# Fallback to domcontentloaded if network doesn't settle
|
|
296
|
+
await page.wait_for_load_state(
|
|
297
|
+
"domcontentloaded", timeout=30000
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Additional wait for JavaScript redirects
|
|
301
|
+
await page.wait_for_timeout(3000)
|
|
302
|
+
|
|
303
|
+
# Check if we navigated
|
|
304
|
+
final_url = page.url
|
|
305
|
+
if final_url != initial_url:
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Successfully navigated: {initial_url} -> {final_url}"
|
|
308
|
+
)
|
|
309
|
+
return True
|
|
310
|
+
else:
|
|
311
|
+
logger.warning("No navigation occurred after click")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Error during click and navigation: {e}")
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# Convenience function for integration
|
|
320
|
+
async def find_and_click_resolver_link_async(page: Page, doi: str) -> Optional[str]:
|
|
321
|
+
"""Find and click the best resolver link.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
page: Playwright page object
|
|
325
|
+
doi: Target DOI
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Final URL after navigation, or None if failed
|
|
329
|
+
"""
|
|
330
|
+
finder = ResolverLinkFinder()
|
|
331
|
+
|
|
332
|
+
# Find link
|
|
333
|
+
link = await finder.find_link_async(page, doi)
|
|
334
|
+
if not link:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
# Click and navigate
|
|
338
|
+
success = await finder.click_and_wait_async(page, link)
|
|
339
|
+
if success:
|
|
340
|
+
return page.url
|
|
341
|
+
else:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
# EOF
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-07-31 00:53:24 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex_repo/src/scitex/scholar/open_url/__init__.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
__FILE__ = (
|
|
11
|
+
"./src/scitex/scholar/open_url/__init__.py"
|
|
12
|
+
)
|
|
13
|
+
__DIR__ = os.path.dirname(__FILE__)
|
|
14
|
+
# ----------------------------------------
|
|
15
|
+
|
|
16
|
+
from ._DOIToURLResolver import DOIToURLResolver
|
|
17
|
+
from ._OpenURLResolver import OpenURLResolver
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"OpenURLResolver",
|
|
21
|
+
"DOIToURLResolver",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# EOF
|
scitex/security/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Reusable security utilities for the SciTeX ecosystem. Works everywhere: local, c
|
|
|
14
14
|
The module is part of the `scitex` package (editable install):
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
cd ~/proj/scitex-
|
|
17
|
+
cd ~/proj/scitex-python
|
|
18
18
|
pip install -e .
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -162,7 +162,7 @@ Total open alerts: 2
|
|
|
162
162
|
```yaml
|
|
163
163
|
- name: Check Security
|
|
164
164
|
run: |
|
|
165
|
-
pip install -e ~/proj/scitex-
|
|
165
|
+
pip install -e ~/proj/scitex-python
|
|
166
166
|
python -m scitex.security.cli check --save
|
|
167
167
|
```
|
|
168
168
|
|
|
@@ -245,7 +245,7 @@ Get path to latest alerts file.
|
|
|
245
245
|
## Testing
|
|
246
246
|
|
|
247
247
|
```bash
|
|
248
|
-
cd ~/proj/scitex-
|
|
248
|
+
cd ~/proj/scitex-python
|
|
249
249
|
pytest tests/test_security.py
|
|
250
250
|
```
|
|
251
251
|
|
scitex/session/README.md
CHANGED
scitex/session/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
2
|
# Timestamp: "2025-08-21 20:36:45 (ywatanabe)"
|
|
4
3
|
# File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/__init__.py
|
|
5
4
|
# ----------------------------------------
|
|
6
5
|
from __future__ import annotations
|
|
6
|
+
|
|
7
7
|
import os
|
|
8
8
|
|
|
9
9
|
__FILE__ = __file__
|
|
@@ -20,27 +20,46 @@ Usage:
|
|
|
20
20
|
import sys
|
|
21
21
|
import matplotlib.pyplot as plt
|
|
22
22
|
from scitex import session
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
# Start a session
|
|
25
25
|
CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = session.start(sys, plt)
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
# Your experiment code here
|
|
28
|
-
|
|
29
|
-
# Close the session
|
|
28
|
+
|
|
29
|
+
# Close the session
|
|
30
30
|
session.close(CONFIG)
|
|
31
31
|
|
|
32
32
|
# Session manager for advanced use cases
|
|
33
33
|
manager = session.SessionManager()
|
|
34
34
|
active_sessions = manager.get_active_sessions()
|
|
35
|
+
|
|
36
|
+
# Using INJECTED sentinel for decorator parameters
|
|
37
|
+
@stx.session
|
|
38
|
+
def main(CONFIG=stx.session.INJECTED, plt=stx.session.INJECTED):
|
|
39
|
+
...
|
|
35
40
|
"""
|
|
36
41
|
|
|
42
|
+
|
|
43
|
+
# Sentinel object for decorator-injected parameters
|
|
44
|
+
class _InjectedSentinel:
|
|
45
|
+
"""Sentinel value indicating a parameter will be injected by a decorator."""
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
return "<INJECTED>"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
INJECTED = _InjectedSentinel()
|
|
52
|
+
|
|
53
|
+
|
|
37
54
|
# Import session management functionality
|
|
55
|
+
from ._decorator import run, session
|
|
56
|
+
from ._lifecycle import close, running2finished, start
|
|
38
57
|
from ._manager import SessionManager
|
|
39
|
-
from ._lifecycle import start, close, running2finished
|
|
40
|
-
from ._decorator import session, run
|
|
41
58
|
|
|
42
59
|
# Export public API
|
|
43
60
|
__all__ = [
|
|
61
|
+
# Sentinel for injected parameters
|
|
62
|
+
"INJECTED",
|
|
44
63
|
# Session lifecycle (main functions)
|
|
45
64
|
"start",
|
|
46
65
|
"close",
|
scitex/session/_decorator.py
CHANGED
|
@@ -21,7 +21,7 @@ import sys as sys_module
|
|
|
21
21
|
|
|
22
22
|
from ._lifecycle import start, close
|
|
23
23
|
from scitex.logging import getLogger
|
|
24
|
-
from
|
|
24
|
+
from . import INJECTED # Use local INJECTED from session module
|
|
25
25
|
|
|
26
26
|
# Internal logger for the decorator itself
|
|
27
27
|
_decorator_logger = getLogger(__name__)
|
scitex/sh/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- ---
|
|
2
2
|
!-- Timestamp: 2025-10-29 07:23:56
|
|
3
3
|
!-- Author: ywatanabe
|
|
4
|
-
!-- File: /home/ywatanabe/proj/scitex-
|
|
4
|
+
!-- File: /home/ywatanabe/proj/scitex-python/src/scitex/sh/README.md
|
|
5
5
|
!-- --- -->
|
|
6
6
|
|
|
7
7
|
# scitex.sh - Shell Command Execution Module
|
scitex/sh/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
2
|
from __future__ import annotations
|
|
3
|
+
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
6
|
__FILE__ = __file__
|
|
@@ -8,9 +8,9 @@ __DIR__ = os.path.dirname(__FILE__)
|
|
|
8
8
|
|
|
9
9
|
from typing import Union
|
|
10
10
|
|
|
11
|
-
from ._types import CommandInput, ReturnFormat, ShellResult
|
|
12
|
-
from ._security import quote, validate_command
|
|
13
11
|
from ._execute import execute
|
|
12
|
+
from ._security import quote, validate_command
|
|
13
|
+
from ._types import CommandInput, ReturnFormat, ShellResult
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def sh(
|
|
@@ -88,6 +88,9 @@ def sh_run(command: CommandInput, verbose: bool = True) -> ShellResult:
|
|
|
88
88
|
return execute(command, verbose=verbose)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
# Legacy functions moved from gen module
|
|
92
|
+
from ._shell_legacy import run_shellcommand, run_shellscript
|
|
93
|
+
|
|
94
|
+
__all__ = ["sh", "sh_run", "quote", "run_shellcommand", "run_shellscript"]
|
|
92
95
|
|
|
93
96
|
# EOF
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-01-22
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/social/__init__.py
|
|
4
|
+
|
|
5
|
+
"""SciTeX Social - Unified social media management.
|
|
6
|
+
|
|
7
|
+
This module provides a thin wrapper around socialia, the core social media
|
|
8
|
+
integration package. It uses scitex branding and environment variable prefixes.
|
|
9
|
+
|
|
10
|
+
Features
|
|
11
|
+
--------
|
|
12
|
+
- Twitter/X posting and analytics
|
|
13
|
+
- LinkedIn posting
|
|
14
|
+
- Reddit posting
|
|
15
|
+
- YouTube analytics
|
|
16
|
+
- Google Analytics integration
|
|
17
|
+
|
|
18
|
+
Environment Variables
|
|
19
|
+
---------------------
|
|
20
|
+
Credentials use SCITEX_SOCIAL_ prefix (falls back to SOCIALIA_):
|
|
21
|
+
- SCITEX_SOCIAL_X_CONSUMER_KEY, SCITEX_SOCIAL_X_CONSUMER_KEY_SECRET
|
|
22
|
+
- SCITEX_SOCIAL_X_ACCESS_TOKEN, SCITEX_SOCIAL_X_ACCESS_TOKEN_SECRET
|
|
23
|
+
- SCITEX_SOCIAL_X_BEARER_TOKEN
|
|
24
|
+
- SCITEX_SOCIAL_LINKEDIN_CLIENT_ID, SCITEX_SOCIAL_LINKEDIN_CLIENT_SECRET
|
|
25
|
+
- SCITEX_SOCIAL_LINKEDIN_ACCESS_TOKEN
|
|
26
|
+
- SCITEX_SOCIAL_REDDIT_CLIENT_ID, SCITEX_SOCIAL_REDDIT_CLIENT_SECRET
|
|
27
|
+
- SCITEX_SOCIAL_YOUTUBE_API_KEY
|
|
28
|
+
- SCITEX_SOCIAL_GOOGLE_ANALYTICS_PROPERTY_ID
|
|
29
|
+
|
|
30
|
+
Usage
|
|
31
|
+
-----
|
|
32
|
+
import scitex as stx
|
|
33
|
+
|
|
34
|
+
# Twitter/X
|
|
35
|
+
x = stx.social.Twitter()
|
|
36
|
+
x.post("Hello from SciTeX!")
|
|
37
|
+
|
|
38
|
+
# LinkedIn
|
|
39
|
+
linkedin = stx.social.LinkedIn()
|
|
40
|
+
linkedin.post("Research update", visibility="public")
|
|
41
|
+
|
|
42
|
+
# YouTube analytics
|
|
43
|
+
yt = stx.social.YouTube()
|
|
44
|
+
stats = yt.get_channel_stats()
|
|
45
|
+
|
|
46
|
+
# Google Analytics
|
|
47
|
+
ga = stx.social.GoogleAnalytics()
|
|
48
|
+
report = ga.get_report(start_date="7daysAgo")
|
|
49
|
+
|
|
50
|
+
See Also
|
|
51
|
+
--------
|
|
52
|
+
- socialia: https://github.com/ywatanabe1989/socialia
|
|
53
|
+
- scitex: https://scitex.ai
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
import os as _os
|
|
57
|
+
|
|
58
|
+
# Set branding BEFORE importing socialia
|
|
59
|
+
_os.environ.setdefault("SOCIALIA_BRAND", "scitex.social")
|
|
60
|
+
_os.environ.setdefault("SOCIALIA_ENV_PREFIX", "SCITEX_SOCIAL")
|
|
61
|
+
|
|
62
|
+
# Check socialia availability
|
|
63
|
+
try:
|
|
64
|
+
import socialia as _socialia
|
|
65
|
+
|
|
66
|
+
# Re-export platform clients
|
|
67
|
+
from socialia import (
|
|
68
|
+
# Content strategies for MCP
|
|
69
|
+
PLATFORM_STRATEGIES,
|
|
70
|
+
# Base class
|
|
71
|
+
BasePoster,
|
|
72
|
+
GoogleAnalytics,
|
|
73
|
+
LinkedIn,
|
|
74
|
+
LinkedInPoster,
|
|
75
|
+
Reddit,
|
|
76
|
+
RedditPoster,
|
|
77
|
+
# Platform clients (preferred names)
|
|
78
|
+
Twitter,
|
|
79
|
+
# Backward compatibility aliases
|
|
80
|
+
TwitterPoster,
|
|
81
|
+
YouTube,
|
|
82
|
+
YouTubePoster,
|
|
83
|
+
)
|
|
84
|
+
from socialia import __version__ as _socialia_version
|
|
85
|
+
|
|
86
|
+
SOCIALIA_AVAILABLE = True
|
|
87
|
+
__socialia_version__ = _socialia_version
|
|
88
|
+
|
|
89
|
+
except ImportError:
|
|
90
|
+
SOCIALIA_AVAILABLE = False
|
|
91
|
+
__socialia_version__ = None
|
|
92
|
+
|
|
93
|
+
# Provide helpful error on access
|
|
94
|
+
class _SocialiaNotAvailable:
|
|
95
|
+
"""Placeholder when socialia is not installed."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, *args, **kwargs):
|
|
98
|
+
raise ImportError(
|
|
99
|
+
"socialia is required for scitex.social. "
|
|
100
|
+
"Install with: pip install socialia"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def __getattr__(self, name):
|
|
104
|
+
raise ImportError(
|
|
105
|
+
"socialia is required for scitex.social. "
|
|
106
|
+
"Install with: pip install socialia"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
BasePoster = _SocialiaNotAvailable
|
|
110
|
+
Twitter = _SocialiaNotAvailable
|
|
111
|
+
LinkedIn = _SocialiaNotAvailable
|
|
112
|
+
Reddit = _SocialiaNotAvailable
|
|
113
|
+
YouTube = _SocialiaNotAvailable
|
|
114
|
+
GoogleAnalytics = _SocialiaNotAvailable
|
|
115
|
+
TwitterPoster = _SocialiaNotAvailable
|
|
116
|
+
LinkedInPoster = _SocialiaNotAvailable
|
|
117
|
+
RedditPoster = _SocialiaNotAvailable
|
|
118
|
+
YouTubePoster = _SocialiaNotAvailable
|
|
119
|
+
PLATFORM_STRATEGIES = ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def has_socialia() -> bool:
|
|
123
|
+
"""Check if socialia is available.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
bool
|
|
128
|
+
True if socialia is installed and importable.
|
|
129
|
+
"""
|
|
130
|
+
return SOCIALIA_AVAILABLE
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
# Availability check
|
|
135
|
+
"SOCIALIA_AVAILABLE",
|
|
136
|
+
"has_socialia",
|
|
137
|
+
"__socialia_version__",
|
|
138
|
+
# Base class
|
|
139
|
+
"BasePoster",
|
|
140
|
+
# Platform clients (preferred names)
|
|
141
|
+
"Twitter",
|
|
142
|
+
"LinkedIn",
|
|
143
|
+
"Reddit",
|
|
144
|
+
"YouTube",
|
|
145
|
+
"GoogleAnalytics",
|
|
146
|
+
# Backward compatibility aliases
|
|
147
|
+
"TwitterPoster",
|
|
148
|
+
"LinkedInPoster",
|
|
149
|
+
"RedditPoster",
|
|
150
|
+
"YouTubePoster",
|
|
151
|
+
# Content strategies
|
|
152
|
+
"PLATFORM_STRATEGIES",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# EOF
|