libreqda 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +481 -0
- libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
- libreqda-1.2.0.dist-info/METADATA +131 -0
- libreqda-1.2.0.dist-info/RECORD +255 -0
- libreqda-1.2.0.dist-info/WHEEL +4 -0
- libreqda-1.2.0.dist-info/entry_points.txt +4 -0
- scripts/babelrc +8 -0
- scripts/dist/LICENSE.txt +33 -0
- scripts/dist/RELEASE.txt +23 -0
- scripts/dist/macos/libreqda.icns +0 -0
- scripts/dist/macos/macos.spec +43 -0
- scripts/dist/macos/plist.patch +10 -0
- scripts/dist/macos/taguette.icns +0 -0
- scripts/dist/macos/taguette_console_wrapper +4 -0
- scripts/dist/macos.sh +32 -0
- scripts/dist/pyinstaller_entrypoint.py +19 -0
- scripts/dist/windows/windows-installer.iss +51 -0
- scripts/dist/windows/windows.spec +39 -0
- scripts/edugain/worker_update_edugain_idps.py +89 -0
- scripts/edugain/worker_update_edugain_idps.sh +9 -0
- scripts/make_password_reset_link.py +56 -0
- scripts/make_theme.py +99 -0
- scripts/new_db_revision.sh +8 -0
- scripts/register_user.py +76 -0
- scripts/update_pot.sh +33 -0
- scripts/update_translations.sh +17 -0
- scripts/validate.py +140 -0
- scripts/wipe_and_disable_user.py +90 -0
- taguette/__init__.py +13 -0
- taguette/access_control.py +145 -0
- taguette/convert.py +623 -0
- taguette/database/__init__.py +177 -0
- taguette/database/base.py +9 -0
- taguette/database/copy.py +433 -0
- taguette/database/models.py +920 -0
- taguette/demodata.py +688 -0
- taguette/export.py +500 -0
- taguette/extract.py +287 -0
- taguette/import_codebook.py +110 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/loki_handler.py +107 -0
- taguette/main.py +649 -0
- taguette/migrations/env.py +72 -0
- taguette/migrations/script.py.mako +24 -0
- taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
- taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
- taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
- taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
- taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
- taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
- taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
- taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
- taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
- taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
- taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
- taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
- taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
- taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
- taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
- taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
- taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
- taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
- taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
- taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
- taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
- taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
- taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
- taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
- taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
- taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
- taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
- taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
- taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
- taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
- taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
- taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
- taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
- taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
- taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
- taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
- taguette/static/README.md +34 -0
- taguette/static/alt_templates/.gitkeep +0 -0
- taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
- taguette/static/css/custom.css +1696 -0
- taguette/static/css/fa-all.min.css +5 -0
- taguette/static/css/themed.css +3 -0
- taguette/static/css/themed.css.map +25 -0
- taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
- taguette/static/css/vendor/bootstrap.min.css +6 -0
- taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
- taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
- taguette/static/css/vendor/hierarchy-select.min.css +1 -0
- taguette/static/css/vendor/introjs.min.css +2 -0
- taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
- taguette/static/favicon.ico +0 -0
- taguette/static/js/codebook-viz.js +428 -0
- taguette/static/js/collection/context-menu.js +180 -0
- taguette/static/js/collection/hierarchy-select.js +71 -0
- taguette/static/js/collection/manager.js +217 -0
- taguette/static/js/document/context-menu.js +92 -0
- taguette/static/js/document/custom-select.js +82 -0
- taguette/static/js/document/manager.js +943 -0
- taguette/static/js/document/treeview.js +593 -0
- taguette/static/js/highlight/README.md +18 -0
- taguette/static/js/highlight/crud.js +119 -0
- taguette/static/js/highlight/extracts.js +497 -0
- taguette/static/js/highlight/loader.js +109 -0
- taguette/static/js/highlight/renderer.js +405 -0
- taguette/static/js/login.js +15 -0
- taguette/static/js/permissions.js +153 -0
- taguette/static/js/polling-event.js +145 -0
- taguette/static/js/project/intro.js +524 -0
- taguette/static/js/project/manager.js +0 -0
- taguette/static/js/project/search.js +124 -0
- taguette/static/js/tag/context-menu.js +133 -0
- taguette/static/js/tag/explore-codes.js +400 -0
- taguette/static/js/tag/hierarchy-select.js +69 -0
- taguette/static/js/tag/manager.js +545 -0
- taguette/static/js/tag/treeview.js +613 -0
- taguette/static/js/taguette.js +1790 -0
- taguette/static/js/utilities.js +714 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
- taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
- taguette/static/js/vendor/hierarchy-select.min.js +1 -0
- taguette/static/js/vendor/introjs.min.js +17 -0
- taguette/static/js/vendor/jquery-3.7.1.js +2 -0
- taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
- taguette/static/libreqda.ico +0 -0
- taguette/static/logo-h.png +0 -0
- taguette/static/logo.png +0 -0
- taguette/static/logo_blanc.png +0 -0
- taguette/static/missing.png +0 -0
- taguette/static/robots.txt +2 -0
- taguette/static/scss/components/_badge.scss +42 -0
- taguette/static/scss/components/_button.scss +169 -0
- taguette/static/scss/components/_checkbox.scss +40 -0
- taguette/static/scss/components/_context_menu.scss +28 -0
- taguette/static/scss/components/_custom_select.scss +150 -0
- taguette/static/scss/components/_dropdown.scss +43 -0
- taguette/static/scss/components/_form.scss +54 -0
- taguette/static/scss/components/_modal.scss +57 -0
- taguette/static/scss/components/_project_card.scss +163 -0
- taguette/static/scss/components/_tag.scss +308 -0
- taguette/static/scss/components/_treeview.scss +76 -0
- taguette/static/scss/custom.scss +809 -0
- taguette/static/scss/layout/_credits.scss +62 -0
- taguette/static/scss/layout/_footer.scss +6 -0
- taguette/static/scss/layout/_navigation.scss +165 -0
- taguette/static/scss/layout/_sidebar.scss +37 -0
- taguette/static/scss/themes/default/_variables.scss +103 -0
- taguette/static/scss/themes/default/theme.css +3 -0
- taguette/static/scss/themes/default/theme.css.map +26 -0
- taguette/static/scss/themes/default/theme.scss +4 -0
- taguette/static/webfonts/BadScript-OFL.txt +94 -0
- taguette/static/webfonts/BadScript.ttf +0 -0
- taguette/static/webfonts/Montserrat-Light.ttf +0 -0
- taguette/static/webfonts/Montserrat-OFL.txt +93 -0
- taguette/static/webfonts/fa-brands-400.eot +0 -0
- taguette/static/webfonts/fa-brands-400.svg +1184 -0
- taguette/static/webfonts/fa-brands-400.ttf +0 -0
- taguette/static/webfonts/fa-brands-400.woff +0 -0
- taguette/static/webfonts/fa-brands-400.woff2 +0 -0
- taguette/static/webfonts/fa-regular-400.eot +0 -0
- taguette/static/webfonts/fa-regular-400.svg +467 -0
- taguette/static/webfonts/fa-regular-400.ttf +0 -0
- taguette/static/webfonts/fa-regular-400.woff +0 -0
- taguette/static/webfonts/fa-regular-400.woff2 +0 -0
- taguette/static/webfonts/fa-solid-900.eot +0 -0
- taguette/static/webfonts/fa-solid-900.svg +2618 -0
- taguette/static/webfonts/fa-solid-900.ttf +0 -0
- taguette/static/webfonts/fa-solid-900.woff +0 -0
- taguette/static/webfonts/fa-solid-900.woff2 +0 -0
- taguette/templates/alt_templates/.gitkeep +0 -0
- taguette/templates/archived_projects.html +50 -0
- taguette/templates/auth/account.html +74 -0
- taguette/templates/auth/cilogon.login.html +89 -0
- taguette/templates/auth/email_reset_password.html +5 -0
- taguette/templates/auth/external_account.html +60 -0
- taguette/templates/auth/login.html +144 -0
- taguette/templates/auth/reset_password.html +27 -0
- taguette/templates/auth/token_needed.html +7 -0
- taguette/templates/base.html +209 -0
- taguette/templates/bug_report.html +158 -0
- taguette/templates/cookies_prompt.html +15 -0
- taguette/templates/downloads.html +127 -0
- taguette/templates/email_bug_report.txt +9 -0
- taguette/templates/email_reset_password.txt +5 -0
- taguette/templates/error.html +8 -0
- taguette/templates/export_codebook.html +20 -0
- taguette/templates/export_document.html +20 -0
- taguette/templates/export_highlights.html +47 -0
- taguette/templates/index.html +347 -0
- taguette/templates/maintenance.html +8 -0
- taguette/templates/messages.js +74 -0
- taguette/templates/modal/change_document.html +68 -0
- taguette/templates/modal/create_collection.html +100 -0
- taguette/templates/modal/create_document.html +84 -0
- taguette/templates/modal/create_highlight.html +38 -0
- taguette/templates/modal/create_tag.html +73 -0
- taguette/templates/modal/explore_codes.html +58 -0
- taguette/templates/modal/manage_collaborators.html +44 -0
- taguette/templates/modal/merge_tag.html +49 -0
- taguette/templates/new_password.html +32 -0
- taguette/templates/partials/alert.html +16 -0
- taguette/templates/partials/codebook_viz_modal.html +34 -0
- taguette/templates/partials/confirm.html +17 -0
- taguette/templates/partials/document_toolbar.html +37 -0
- taguette/templates/partials/hlinfo.html +13 -0
- taguette/templates/partials/project_card.html +44 -0
- taguette/templates/partials/project_card_archived.html +41 -0
- taguette/templates/partials/project_sidebar.html +99 -0
- taguette/templates/partials/spinner.html +11 -0
- taguette/templates/permissions.html +54 -0
- taguette/templates/project.html +288 -0
- taguette/templates/project_delete.html +17 -0
- taguette/templates/project_import.html +122 -0
- taguette/templates/project_import_codebook.html +34 -0
- taguette/templates/project_import_codebook_confirm.html +49 -0
- taguette/templates/project_new.html +31 -0
- taguette/templates/tos.html +8 -0
- taguette/templates/trans.js +7 -0
- taguette/templates/welcome.html +15 -0
- taguette/utils.py +177 -0
- taguette/validate.py +141 -0
- taguette/version_check.py +102 -0
- taguette/web/__init__.py +252 -0
- taguette/web/api.py +1835 -0
- taguette/web/base.py +864 -0
- taguette/web/export.py +338 -0
- taguette/web/views.py +1618 -0
taguette/extract.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from bs4 import BeautifulSoup, NavigableString
|
|
2
|
+
import opentelemetry.trace
|
|
3
|
+
import prometheus_client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
tracer = opentelemetry.trace.get_tracer(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
BUCKETS = [0.001, 0.002, 0.003, 0.004,
|
|
10
|
+
0.006, 0.008, 0.010,
|
|
11
|
+
0.015, 0.020, 0.025, 0.030, 0.035, 0.04, 0.045, 0.05,
|
|
12
|
+
0.06, 0.07, 0.08, 0.09, 0.10, 0.15, 0.20, 0.5, 1.0]
|
|
13
|
+
PROM_EXTRACT_TIME = prometheus_client.Histogram(
|
|
14
|
+
'html_extract_seconds',
|
|
15
|
+
"Time to extract part of an HTML document (extract.extract())",
|
|
16
|
+
buckets=BUCKETS,
|
|
17
|
+
)
|
|
18
|
+
PROM_HIGHLIGHT_TIME = prometheus_client.Histogram(
|
|
19
|
+
'html_highlight_seconds',
|
|
20
|
+
"Time to add highlight tags to an HTML document (extract.highlight())",
|
|
21
|
+
buckets=BUCKETS,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def split_utf8(s, pos):
|
|
26
|
+
"""Split a string at a given UTF-8 byte position.
|
|
27
|
+
|
|
28
|
+
If the position falls inside of a codepoint, the string will be split after
|
|
29
|
+
that codepoint.
|
|
30
|
+
"""
|
|
31
|
+
s = s.encode('utf-8')
|
|
32
|
+
while pos < len(s) and 0x80 <= s[pos] <= 0xBF:
|
|
33
|
+
pos += 1
|
|
34
|
+
return s[:pos].decode('utf-8'), s[pos:].decode('utf-8')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_pos(node, offset, after):
|
|
38
|
+
"""Find a position in an HTML document.
|
|
39
|
+
"""
|
|
40
|
+
indices = []
|
|
41
|
+
while True:
|
|
42
|
+
if getattr(node, 'contents', None):
|
|
43
|
+
indices.append(0)
|
|
44
|
+
node = node.contents[0]
|
|
45
|
+
else:
|
|
46
|
+
if isinstance(node, NavigableString):
|
|
47
|
+
nb = len(node.string.encode('utf-8'))
|
|
48
|
+
if (after and nb > offset) or (not after and nb >= offset):
|
|
49
|
+
break
|
|
50
|
+
else:
|
|
51
|
+
offset -= nb
|
|
52
|
+
while not node.next_sibling:
|
|
53
|
+
indices.pop()
|
|
54
|
+
node = node.parent
|
|
55
|
+
indices[-1] += 1
|
|
56
|
+
node = node.next_sibling
|
|
57
|
+
return node, offset, indices
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def delete_left(node, indices):
|
|
61
|
+
"""Delete the left part of an HTML tree.
|
|
62
|
+
"""
|
|
63
|
+
for idx in indices:
|
|
64
|
+
for child in node.contents[:idx]:
|
|
65
|
+
child.extract()
|
|
66
|
+
node = node.contents[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def delete_right(node, indices):
|
|
70
|
+
"""Delete the right part of an HTML tree.
|
|
71
|
+
"""
|
|
72
|
+
for idx in indices:
|
|
73
|
+
del node.contents[idx + 1:]
|
|
74
|
+
node = node.contents[idx]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@tracer.start_as_current_span('taguette/extract')
|
|
78
|
+
@PROM_EXTRACT_TIME.time()
|
|
79
|
+
def extract(html, start, end):
|
|
80
|
+
"""Extract a snippet out of an HTML document.
|
|
81
|
+
|
|
82
|
+
Locations are computed over UTF-8 bytes, and doesn't count HTML tags.
|
|
83
|
+
|
|
84
|
+
Extraction is aware of tags, so:
|
|
85
|
+
|
|
86
|
+
>>> '<p><u>Hello</u> there <i>World</i></p>'[17:27]
|
|
87
|
+
'here <i>Wo'
|
|
88
|
+
>>> extract('<p><u>Hello</u> there <i>World</i></p>', 7, 14)
|
|
89
|
+
'<p>here <i>Wo</i></p>'
|
|
90
|
+
"""
|
|
91
|
+
soup = BeautifulSoup(html, 'html5lib')
|
|
92
|
+
|
|
93
|
+
# Trim the right side first, because that doesn't mess our start position
|
|
94
|
+
if end is not None:
|
|
95
|
+
e = find_pos(soup, end, False)
|
|
96
|
+
e[0].replace_with(NavigableString(split_utf8(e[0].string, e[1])[0]))
|
|
97
|
+
delete_right(soup, e[2])
|
|
98
|
+
|
|
99
|
+
# Trim the left side
|
|
100
|
+
if start is not None:
|
|
101
|
+
s = find_pos(soup, start, True)
|
|
102
|
+
s[0].replace_with(NavigableString(split_utf8(s[0].string, s[1])[1]))
|
|
103
|
+
delete_left(soup, s[2])
|
|
104
|
+
|
|
105
|
+
# Remove everything but body
|
|
106
|
+
body = soup.body
|
|
107
|
+
soup.clear()
|
|
108
|
+
soup.append(body)
|
|
109
|
+
|
|
110
|
+
# Remove the body tag itself to only have the contents
|
|
111
|
+
soup.body.unwrap()
|
|
112
|
+
|
|
113
|
+
# Back to text
|
|
114
|
+
return str(soup)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def byte_to_str_index(string, byte_index):
|
|
118
|
+
"""Converts a byte index in the UTF-8 string into a codepoint index.
|
|
119
|
+
|
|
120
|
+
If the index falls inside of a codepoint unit, it will be rounded down.
|
|
121
|
+
"""
|
|
122
|
+
for idx, char in enumerate(string):
|
|
123
|
+
char_size = len(char.encode('utf-8'))
|
|
124
|
+
if char_size > byte_index:
|
|
125
|
+
return idx
|
|
126
|
+
byte_index -= char_size
|
|
127
|
+
return len(string)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@tracer.start_as_current_span('taguette/highlight')
|
|
131
|
+
@PROM_HIGHLIGHT_TIME.time()
|
|
132
|
+
def highlight(html, highlights, show_tags=False):
|
|
133
|
+
"""Highlight part of an HTML documents.
|
|
134
|
+
|
|
135
|
+
:param highlights: Iterable of (start, end, tags) triples, which are
|
|
136
|
+
computed over UTF-8 bytes and don't count HTML tags
|
|
137
|
+
:param show_tags: Whether to show the tag names within brackets after each
|
|
138
|
+
highlight
|
|
139
|
+
"""
|
|
140
|
+
# Build a list of starting points and ending points
|
|
141
|
+
starts = []
|
|
142
|
+
ends = []
|
|
143
|
+
for hl in highlights:
|
|
144
|
+
starts.append((hl[0], 'start', []))
|
|
145
|
+
if len(hl) == 2:
|
|
146
|
+
ends.append((hl[1], 'end', []))
|
|
147
|
+
else:
|
|
148
|
+
ends.append((hl[1], 'end', hl[2]))
|
|
149
|
+
# This relies on the fact that 'end' < 'start'
|
|
150
|
+
events = sorted(ends + starts)
|
|
151
|
+
|
|
152
|
+
events = iter(events)
|
|
153
|
+
soup = BeautifulSoup(html, 'html5lib')
|
|
154
|
+
|
|
155
|
+
pos = 0
|
|
156
|
+
node = soup
|
|
157
|
+
highlighting = 0
|
|
158
|
+
try:
|
|
159
|
+
event_pos, event_type, tags = next(events)
|
|
160
|
+
except StopIteration:
|
|
161
|
+
event_pos = event_type = tags = None
|
|
162
|
+
|
|
163
|
+
while node is not None:
|
|
164
|
+
if getattr(node, 'contents', None):
|
|
165
|
+
# Move down
|
|
166
|
+
node = node.contents[0]
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if isinstance(node, NavigableString):
|
|
170
|
+
# Move through text
|
|
171
|
+
nb = len(node.string.encode('utf-8'))
|
|
172
|
+
while event_pos is not None:
|
|
173
|
+
if event_pos == pos and event_type == 'start':
|
|
174
|
+
# Start highlighting at beginning of text node
|
|
175
|
+
highlighting += 1
|
|
176
|
+
try:
|
|
177
|
+
event_pos, event_type, tags = next(events)
|
|
178
|
+
except StopIteration:
|
|
179
|
+
event_pos = None
|
|
180
|
+
elif pos + nb > event_pos:
|
|
181
|
+
# Next event falls inside of this text node
|
|
182
|
+
if event_type == 'start' and highlighting:
|
|
183
|
+
# Keep highlighting (can't highlight *more*)
|
|
184
|
+
highlighting += 1
|
|
185
|
+
elif (
|
|
186
|
+
event_type == 'end'
|
|
187
|
+
and not show_tags
|
|
188
|
+
and highlighting > 1
|
|
189
|
+
):
|
|
190
|
+
# Keep highlighting (no need to put labels)
|
|
191
|
+
highlighting -= 1
|
|
192
|
+
else: # 'end' and (show_tags or highlighting becomes 0)
|
|
193
|
+
# Split it
|
|
194
|
+
char_idx = byte_to_str_index(
|
|
195
|
+
node.string,
|
|
196
|
+
event_pos - pos,
|
|
197
|
+
)
|
|
198
|
+
left = node.string[:char_idx]
|
|
199
|
+
right = node.string[char_idx:]
|
|
200
|
+
|
|
201
|
+
# Left part
|
|
202
|
+
newnode = NavigableString(left)
|
|
203
|
+
if highlighting:
|
|
204
|
+
# Optionally highlight left part
|
|
205
|
+
span = soup.new_tag(
|
|
206
|
+
'span',
|
|
207
|
+
attrs={'class': 'highlight'},
|
|
208
|
+
)
|
|
209
|
+
span.append(newnode)
|
|
210
|
+
newnode = span
|
|
211
|
+
node.replace_with(newnode)
|
|
212
|
+
node = newnode
|
|
213
|
+
|
|
214
|
+
if event_type == 'start':
|
|
215
|
+
highlighting += 1
|
|
216
|
+
else:
|
|
217
|
+
highlighting -= 1
|
|
218
|
+
if show_tags:
|
|
219
|
+
# Add tag labels
|
|
220
|
+
comment = soup.new_tag(
|
|
221
|
+
'span',
|
|
222
|
+
attrs={'class': 'taglist'},
|
|
223
|
+
)
|
|
224
|
+
comment.string = ' [%s]' % ', '.join(tags)
|
|
225
|
+
node.insert_after(comment)
|
|
226
|
+
node = comment
|
|
227
|
+
|
|
228
|
+
# Right part
|
|
229
|
+
newnode = NavigableString(right)
|
|
230
|
+
node.insert_after(newnode)
|
|
231
|
+
node = newnode
|
|
232
|
+
nb -= event_pos - pos
|
|
233
|
+
pos = event_pos
|
|
234
|
+
# Next loop will highlight right part if needed
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
event_pos, event_type, tags = next(events)
|
|
238
|
+
except StopIteration:
|
|
239
|
+
event_pos = None
|
|
240
|
+
elif highlighting: # and pos + nb <= event_pos:
|
|
241
|
+
# Highlight whole text node
|
|
242
|
+
newnode = soup.new_tag(
|
|
243
|
+
'span',
|
|
244
|
+
attrs={'class': 'highlight'},
|
|
245
|
+
)
|
|
246
|
+
node.replace_with(newnode)
|
|
247
|
+
newnode.append(node)
|
|
248
|
+
node = newnode
|
|
249
|
+
if pos + nb == event_pos and event_type == 'end':
|
|
250
|
+
if show_tags:
|
|
251
|
+
comment = soup.new_tag(
|
|
252
|
+
'span',
|
|
253
|
+
attrs={'class': 'taglist'},
|
|
254
|
+
)
|
|
255
|
+
comment.string = ' [%s]' % ', '.join(tags)
|
|
256
|
+
newnode.insert_after(comment)
|
|
257
|
+
node = comment
|
|
258
|
+
highlighting -= 1
|
|
259
|
+
try:
|
|
260
|
+
event_pos, event_type, tags = next(events)
|
|
261
|
+
except StopIteration:
|
|
262
|
+
event_pos = None
|
|
263
|
+
break
|
|
264
|
+
else: # not highlighting and pos + nb <= event_pos
|
|
265
|
+
# Skip whole text node
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
pos += nb
|
|
269
|
+
|
|
270
|
+
# Move up until there's a sibling
|
|
271
|
+
while not node.next_sibling and node.parent:
|
|
272
|
+
node = node.parent
|
|
273
|
+
if not node.parent:
|
|
274
|
+
break
|
|
275
|
+
# Move to next node
|
|
276
|
+
node = node.next_sibling
|
|
277
|
+
|
|
278
|
+
# Remove everything but body
|
|
279
|
+
body = soup.body
|
|
280
|
+
soup.clear()
|
|
281
|
+
soup.append(body)
|
|
282
|
+
|
|
283
|
+
# Remove the body tag itself to only have the contents
|
|
284
|
+
soup.body.unwrap()
|
|
285
|
+
|
|
286
|
+
# Back to text
|
|
287
|
+
return str(soup)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import chardet
|
|
2
|
+
import codecs
|
|
3
|
+
import csv
|
|
4
|
+
|
|
5
|
+
from .utils import _f
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidCodebook(ValueError):
|
|
9
|
+
"""File can't be read as a codebook.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message, row=None):
|
|
13
|
+
super(InvalidCodebook, self).__init__(message)
|
|
14
|
+
self.message = message
|
|
15
|
+
self.row = row
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_tags_csv(reader):
|
|
19
|
+
detector = chardet.UniversalDetector()
|
|
20
|
+
chunk = reader.read(4096)
|
|
21
|
+
while chunk and not detector.done:
|
|
22
|
+
detector.feed(chunk)
|
|
23
|
+
chunk = reader.read(4096)
|
|
24
|
+
detector.close()
|
|
25
|
+
charset = detector.result['encoding'] or 'utf-8'
|
|
26
|
+
reader.seek(0, 0)
|
|
27
|
+
|
|
28
|
+
reader = codecs.getreader(charset)(reader)
|
|
29
|
+
reader = csv.reader(reader)
|
|
30
|
+
|
|
31
|
+
# Read header
|
|
32
|
+
try:
|
|
33
|
+
header = next(reader)
|
|
34
|
+
except (StopIteration, csv.Error, UnicodeDecodeError):
|
|
35
|
+
raise InvalidCodebook(_f("Invalid file: CSV expected"))
|
|
36
|
+
header = [cell.lower() for cell in header]
|
|
37
|
+
|
|
38
|
+
# Figure out which column has the tag path
|
|
39
|
+
expected = ('tag' in header) + ('name' in header) + ('path' in header)
|
|
40
|
+
if expected > 1:
|
|
41
|
+
raise InvalidCodebook(
|
|
42
|
+
_f("Not sure which column to use for tag name"),
|
|
43
|
+
)
|
|
44
|
+
elif expected == 0:
|
|
45
|
+
raise InvalidCodebook(_f("No 'tag', 'name', or 'path' column"))
|
|
46
|
+
else:
|
|
47
|
+
if 'tag' in header:
|
|
48
|
+
col_path = header.index('tag')
|
|
49
|
+
elif 'name' in header:
|
|
50
|
+
col_path = header.index('name')
|
|
51
|
+
else:
|
|
52
|
+
col_path = header.index('path')
|
|
53
|
+
|
|
54
|
+
# Is there a description?
|
|
55
|
+
try:
|
|
56
|
+
col_description = header.index('description')
|
|
57
|
+
except ValueError:
|
|
58
|
+
col_description = None
|
|
59
|
+
|
|
60
|
+
# Is there a color?
|
|
61
|
+
try:
|
|
62
|
+
col_color = header.index('color')
|
|
63
|
+
except ValueError:
|
|
64
|
+
col_color = None
|
|
65
|
+
|
|
66
|
+
# Is there a parent?
|
|
67
|
+
try:
|
|
68
|
+
col_parent = header.index('parent')
|
|
69
|
+
except ValueError:
|
|
70
|
+
col_parent = None
|
|
71
|
+
|
|
72
|
+
needed_rows = max(
|
|
73
|
+
num + 1
|
|
74
|
+
for num in (col_path, col_color, col_parent, col_description)
|
|
75
|
+
if num is not None
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Read file
|
|
79
|
+
try:
|
|
80
|
+
tags = []
|
|
81
|
+
for nb, row in enumerate(reader, 2):
|
|
82
|
+
if len(row) < needed_rows:
|
|
83
|
+
raise InvalidCodebook(_f("Not enough columns"), nb)
|
|
84
|
+
if not any(row):
|
|
85
|
+
continue # Skip empty row
|
|
86
|
+
path = row[col_path].strip()
|
|
87
|
+
if not path:
|
|
88
|
+
raise InvalidCodebook(_f("Empty tag name"), nb)
|
|
89
|
+
if col_description is not None:
|
|
90
|
+
description = row[col_description].strip()
|
|
91
|
+
else:
|
|
92
|
+
description = ''
|
|
93
|
+
if col_color is not None:
|
|
94
|
+
color = row[col_color].strip()
|
|
95
|
+
else:
|
|
96
|
+
color = ''
|
|
97
|
+
if col_parent is not None:
|
|
98
|
+
parent = row[col_parent].strip()
|
|
99
|
+
else:
|
|
100
|
+
parent = ''
|
|
101
|
+
tags.append({'path': path, 'color': color,
|
|
102
|
+
'parent': parent, 'description': description})
|
|
103
|
+
except (csv.Error, UnicodeDecodeError):
|
|
104
|
+
raise InvalidCodebook(_f("Invalid CSV file"))
|
|
105
|
+
|
|
106
|
+
return tags
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def list_tags(reader):
|
|
110
|
+
return list_tags_csv(reader)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
taguette/loki_handler.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LokiHandler(logging.Handler):
|
|
7
|
+
|
|
8
|
+
def __init__(self, loki_url: str, job_name: str = "libreqda"):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.loki_url = f"{loki_url}/loki/api/v1/push"
|
|
11
|
+
self.job_name = job_name
|
|
12
|
+
self.buffer = []
|
|
13
|
+
self.buffer_size = 10
|
|
14
|
+
|
|
15
|
+
def emit(self, record):
|
|
16
|
+
try:
|
|
17
|
+
message = self.format(record)
|
|
18
|
+
level = record.levelname.lower()
|
|
19
|
+
|
|
20
|
+
labels = {
|
|
21
|
+
"job": self.job_name,
|
|
22
|
+
"level": level,
|
|
23
|
+
"logger": record.name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if hasattr(record, 'user_id'):
|
|
27
|
+
labels["user_id"] = str(record.user_id)
|
|
28
|
+
if hasattr(record, 'project_id'):
|
|
29
|
+
labels["project_id"] = str(record.project_id)
|
|
30
|
+
|
|
31
|
+
timestamp = str(int(time.time() * 1000000000))
|
|
32
|
+
|
|
33
|
+
self.buffer.append({
|
|
34
|
+
"stream": labels,
|
|
35
|
+
"values": [[timestamp, message]]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if len(self.buffer) >= self.buffer_size:
|
|
39
|
+
self._send_buffer()
|
|
40
|
+
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def _send_buffer(self):
|
|
45
|
+
if not self.buffer:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
payload = {"streams": self.buffer}
|
|
50
|
+
response = requests.post(
|
|
51
|
+
self.loki_url,
|
|
52
|
+
json=payload,
|
|
53
|
+
timeout=5
|
|
54
|
+
)
|
|
55
|
+
if response.status_code == 204:
|
|
56
|
+
self.buffer.clear()
|
|
57
|
+
except Exception:
|
|
58
|
+
self.buffer.clear()
|
|
59
|
+
|
|
60
|
+
def close(self):
|
|
61
|
+
self._send_buffer()
|
|
62
|
+
super().close()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_loki_connectivity(loki_url: str):
|
|
66
|
+
"""Test if Loki is accessible and ready"""
|
|
67
|
+
try:
|
|
68
|
+
import requests
|
|
69
|
+
response = requests.get(f"{loki_url}/ready", timeout=5)
|
|
70
|
+
if response.status_code == 200:
|
|
71
|
+
return True, "Loki is ready"
|
|
72
|
+
else:
|
|
73
|
+
return False, f"Loki not ready (status: {response.status_code})"
|
|
74
|
+
except requests.exceptions.ConnectionError:
|
|
75
|
+
return False, f"Loki not accessible at {loki_url}"
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return False, f"Loki test error: {e}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def setup_loki_logging(loki_url: str):
|
|
81
|
+
# Test connectivity first
|
|
82
|
+
is_ready, message = test_loki_connectivity(loki_url)
|
|
83
|
+
logger = logging.getLogger(__name__)
|
|
84
|
+
|
|
85
|
+
if is_ready:
|
|
86
|
+
logger.info(message)
|
|
87
|
+
else:
|
|
88
|
+
logger.warning(message)
|
|
89
|
+
|
|
90
|
+
loki_handler = LokiHandler(loki_url)
|
|
91
|
+
loki_handler.setLevel(logging.WARNING)
|
|
92
|
+
formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
|
|
93
|
+
loki_handler.setFormatter(formatter)
|
|
94
|
+
|
|
95
|
+
# Filter out tornado.access logs from Loki
|
|
96
|
+
class ExcludeTornadoAccess(logging.Filter):
|
|
97
|
+
def filter(self, record):
|
|
98
|
+
return not record.name.startswith("tornado.access")
|
|
99
|
+
loki_handler.addFilter(ExcludeTornadoAccess())
|
|
100
|
+
|
|
101
|
+
root_logger = logging.getLogger()
|
|
102
|
+
root_logger.addHandler(loki_handler)
|
|
103
|
+
|
|
104
|
+
# Test log
|
|
105
|
+
logger.info("Loki test log - %s", loki_url)
|
|
106
|
+
|
|
107
|
+
return loki_handler
|