Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions .github/workflows/build_exe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ name: Build Executables

on:
workflow_call:
secrets:
APPLE_DEV_EMAIL:
APP_SPEC_PASS:
DEV_APP_CERT:
DEV_APP_CERT_PASS:
DEV_INST_CERT:
DEV_INST_CERT_PASS:
APPLE_KEY_PASS:
APPLE_APP_CERT_ID:
APPLE_INST_CERT_ID:
TEAM_ID:

inputs:
is_release:
default: false
Expand Down Expand Up @@ -62,7 +74,7 @@ jobs:
echo "PYINST_BIN=\"$(which pyinstaller)\"" >> "$GITHUB_ENV"
- name: Create macOS keychain
id: keychain
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
env:
APPLE_DEV_EMAIL: ${{secrets.APPLE_DEV_EMAIL}}
APP_SPEC_PASS: ${{secrets.APP_SPEC_PASS}}
Expand Down Expand Up @@ -113,7 +125,7 @@ jobs:
run: ${{ matrix.sha_command }}

- name: Create and sign mac package
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
env:
APPLE_INST_CERT_ID: ${{secrets.APPLE_INST_CERT_ID}}
APPLE_KEYCHAIN_PASS: ${{secrets.APPLE_KEY_PASS}}
Expand All @@ -127,27 +139,27 @@ jobs:
pkgbuild --identifier "org.mangotango.cli" --timestamp --root /tmp/mangotango --install-location /Applications "./dist/mangotango_${{matrix.artifact_name}}_signed.pkg" --sign "$APPLE_INST_CERT_ID"

- name: Notarize Mac package
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
env:
APPLE_DEV_EMAIL: ${{secrets.APPLE_DEV_EMAIL}}
APPLE_TEAM_ID: ${{secrets.TEAM_ID}}
APP_SPEC_PASS: ${{secrets.APP_SPEC_PASS}}
run: xcrun notarytool submit dist/mangotango_${{matrix.artifact_name}}_signed.pkg --apple-id $APPLE_DEV_EMAIL --team-id $APPLE_TEAM_ID --password $APP_SPEC_PASS --wait > notarization_output.txt
run: xcrun notarytool submit dist/mangotango_${{matrix.artifact_name}}_signed.pkg --apple-id $APPLE_DEV_EMAIL --team-id $APPLE_TEAM_ID --password $APP_SPEC_PASS --wait

- name: Staple the notarization ticket
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
run: xcrun stapler staple dist/mangotango_${{matrix.artifact_name}}_signed.pkg

- name: Clean up macOS Artifacts
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
run: |
rm -rf /tmp/mangotango
rm -rf dist/mangotango_${{matrix.artifact_name}}
rm -rf dist/mangotango_${{matrix.artifact_name}}.pkg
mv dist/mangotango_${{matrix.artifact_name}}_signed.pkg dist/mangotango_${{matrix.artifact_name}}.pkg

- name: Compute the SHA1 hashsum for macOS .pkg
if: runner.os == 'macOS'
if: runner.os == 'macOS' && inputs.is_release
run: ${{ matrix.sha_command_pkg }}

- name: Inspect the dist/ directory before uploading artifacts
Expand All @@ -160,4 +172,3 @@ jobs:
name: ${{ matrix.artifact_name }}
path: |
dist/*

27 changes: 26 additions & 1 deletion analyzer_interface/context.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from abc import ABC, abstractmethod
from typing import TypeVar
from typing import Any, Callable, Optional, TypeVar, Union

import polars as pl
from dash import Dash
from polars import DataFrame
from pydantic import BaseModel
from shiny import Inputs, Outputs, Session
from shiny.ui._navs import NavPanel

from .interface import SecondaryAnalyzerInterface
from .params import ParamValue
Expand Down Expand Up @@ -155,3 +158,25 @@ def parquet_path(self) -> str:
file to it.
"""
pass


ServerCallback = Union[
Callable[[Inputs], None], Callable[[Inputs, Outputs, Session], None]
]


class ShinyContext(BaseModel):
panel: NavPanel = None
server_handler: Optional[ServerCallback] = None

class Config:
arbitrary_types_allowed = True


class FactoryOutputContext(BaseModel):
shiny: Optional[ShinyContext] = None
api: Optional[dict[str, Any]] = None
data_frames: Optional[dict[str, DataFrame]] = None

class Config:
arbitrary_types_allowed = True
19 changes: 14 additions & 5 deletions analyzer_interface/declaration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Callable
from typing import Callable, Union

from .context import (
FactoryOutputContext,
PrimaryAnalyzerContext,
SecondaryAnalyzerContext,
WebPresenterContext,
Expand Down Expand Up @@ -69,11 +70,17 @@ def __init__(self, interface: SecondaryAnalyzerInterface, main: Callable):


class WebPresenterDeclaration(WebPresenterInterface):
factory: Callable[["WebPresenterContext"], None]

factory: Callable[["WebPresenterContext"], Union[FactoryOutputContext, None]]
shiny: bool
server_name: str

def __init__(self, interface: WebPresenterInterface, factory: Callable, name: str):
def __init__(
self,
interface: WebPresenterInterface,
factory: Callable,
name: str,
shiny: bool,
):
"""Creates a web presenter declaration

Args:
Expand All @@ -99,4 +106,6 @@ def __init__(self, interface: WebPresenterInterface, factory: Callable, name: st
https://docs.python.org/3/tutorial/modules.html

"""
super().__init__(**interface.model_dump(), factory=factory, server_name=name)
super().__init__(
**interface.model_dump(), factory=factory, server_name=name, shiny=shiny
)
1 change: 1 addition & 0 deletions analyzers/example/example_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
# You must pass __name__ here. It's to make Dash happy.
# See: http://dash.plotly.com/urls
name=__name__,
shiny=False,
)
2 changes: 1 addition & 1 deletion analyzers/hashtags_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
from .interface import interface

hashtags_web = WebPresenterDeclaration(
interface=interface, factory=factory, name=__name__
interface=interface, factory=factory, name=__name__, shiny=True
)
54 changes: 6 additions & 48 deletions analyzers/hashtags_web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

import numpy as np
import polars as pl
from shiny import App, reactive, render, ui
from shiny import reactive, render, ui
from shinywidgets import output_widget, render_widget

from ..hashtags.interface import COL_AUTHOR_ID, COL_POST, COL_TIME
from .analysis import secondary_analyzer
from .plots import plot_bar_plotly, plot_gini_plotly, plot_users_plotly

MANGO_ORANGE2 = "#f3921e"
LOGO_URL = "https://raw.githubusercontent.com/CIB-Mango-Tree/CIB-Mango-Tree-Website/main/assets/images/mango-text.PNG"

# https://icons.getbootstrap.com/icons/question-circle-fill/
question_circle_fill = ui.HTML(
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle-fill mb-1" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247zm2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg>'
)

df_global = None
context_global = None
df_raw = None


def _set_df_global_state(df_input, df_output):
def set_df_global_state(df_input, df_output):
global df_global, df_raw
df_global = df_output
df_raw = df_input # Will be loaded from context when needed
Expand All @@ -36,9 +37,6 @@ def get_raw_data_subset(time_start, time_end, user_id, hashtag):


# Global variables for CLI integration
df_global = None
context_global = None
df_raw = None


def select_users(secondary_output, selected_hashtag):
Expand Down Expand Up @@ -154,46 +152,6 @@ def select_users(secondary_output, selected_hashtag):
ui.output_data_frame("tweets"),
)

analysis_panel_elements = [
page_dependencies,
analysis_panel,
ui.layout_columns(
hashtag_plot_panel,
users_plot_panel,
),
tweet_explorer,
]


ABOUT_TEXT = ui.markdown(
f"""

<img src="{LOGO_URL}" alt="logo" style="width:200px;"/>

CIB Mango Tree, a collaborative and open-source project to develop software that tests for coordinated inauthentic behavior (CIB) in datasets of online activity.

[mangotree.org](https://mangotree.org)

A project of [Civic Tech DC](https://www.civictechdc.org/), our mission is to share methods to uncover how disruptive actors seek to hack our legitimate online discourse regarding health, politics, and society. The CIB Mango Tree presents the most simple tests for CIB first – the low-hanging fruit. These tests are easy to run and interpret. They will reveal signs of unsophisticated CIB. As you move up the Mango Tree, tests become harder and will scavenge for higher-hanging fruit.

"""
)
app_layout = ui.page_navbar(
ui.nav_panel(
"Dashboard",
analysis_panel_elements,
),
ui.nav_panel(
"About",
ui.card(
ui.card_header("About the Mango Tree project"),
ABOUT_TEXT,
ui.card_footer("PolyForm Noncommercial License 1.0.0"),
),
),
title="Hashtag analysis dashboard",
)


def server(input, output, session):
@reactive.calc
Expand Down
65 changes: 37 additions & 28 deletions analyzers/hashtags_web/factory.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import polars as pl
from pydantic import BaseModel
from dash import html
from shiny.ui import layout_columns, nav_panel

from analyzer_interface.context import WebPresenterContext
from analyzer_interface.context import (
FactoryOutputContext,
ShinyContext,
WebPresenterContext,
)
from app.shiny import page_dependencies

from ..hashtags.interface import COL_AUTHOR_ID, COL_POST, COL_TIME, OUTPUT_GINI
from .app import _set_df_global_state, app_layout, server


# config used by AnalysisWebServerContext._start_shiny_server()
class ShinyServerConfig(BaseModel):
server: object
port: int = 8051


class ShinyAppConfig(BaseModel):
app_layout: object
server_config: ShinyServerConfig
from .app import (
analysis_panel,
hashtag_plot_panel,
server,
set_df_global_state,
tweet_explorer,
users_plot_panel,
)


def factory(
web_context: WebPresenterContext,
):
) -> FactoryOutputContext:
# Load the primary output associated with this project
df_hashtags = pl.read_parquet(web_context.base.table(OUTPUT_GINI).parquet_path)

Expand All @@ -41,26 +43,33 @@ def factory(
pl.col(COL_TIME)
)

# Create app layout and server with processed data
_set_df_global_state(df_input=df_raw, df_output=df_hashtags)

# For now, mount the Shiny app via iframe in Dash
# This preserves the existing CLI integration
from dash import html

app = web_context.dash_app
set_df_global_state(
df_input=df_raw,
df_output=df_hashtags,
)

app.layout = html.Div(
web_context.dash_app.layout = html.Div(
[
html.H1("Hashtag Analysis Dashboard"),
html.Iframe(
src="http://localhost:8051", # Shiny app will run on port 8051
src="http://localhost:8050/shiny", # Shiny app will run on port 8051
style={"width": "100%", "height": "800px", "border": "none"},
),
]
)

# Return Shiny app configuration for the webserver context to handle
return ShinyAppConfig(
app_layout=app_layout, server_config=ShinyServerConfig(server=server, port=8051)
return FactoryOutputContext(
shiny=ShinyContext(
server_handler=server,
panel=nav_panel(
"Dashboard",
page_dependencies,
analysis_panel,
layout_columns(
hashtag_plot_panel,
users_plot_panel,
),
tweet_explorer,
),
)
)
2 changes: 1 addition & 1 deletion analyzers/ngram_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
from .interface import interface

ngrams_web = WebPresenterDeclaration(
interface=interface, factory=factory, name=__name__
interface=interface, factory=factory, name=__name__, shiny=False
)
2 changes: 1 addition & 1 deletion analyzers/temporal_barplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
from .interface import interface

temporal_barplot = WebPresenterDeclaration(
interface=interface, factory=factory, name=__name__
interface=interface, factory=factory, name=__name__, shiny=False
)
Loading