Full Code of MycroftAI/selene-backend for AI

master e10ac91cde6b cached
538 files
1001.5 KB
242.5k tokens
1296 symbols
1 requests
Download .txt
Showing preview only (1,140K chars total). Download the full file or copy to clipboard to get everything.
Repository: MycroftAI/selene-backend
Branch: master
Commit: e10ac91cde6b
Files: 538
Total size: 1001.5 KB

Directory structure:
gitextract_zfonubvk/

├── .editorconfig
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── SUPPORT.md
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── README.md
├── api/
│   ├── account/
│   │   ├── account_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── change_email_address.py
│   │   │       ├── change_password.py
│   │   │       ├── city.py
│   │   │       ├── country.py
│   │   │       ├── defaults.py
│   │   │       ├── device.py
│   │   │       ├── device_count.py
│   │   │       ├── geography.py
│   │   │       ├── membership.py
│   │   │       ├── pairing_code.py
│   │   │       ├── preferences.py
│   │   │       ├── region.py
│   │   │       ├── skill_oauth.py
│   │   │       ├── skill_settings.py
│   │   │       ├── skills.py
│   │   │       ├── software_update.py
│   │   │       ├── ssh_key_validator.py
│   │   │       ├── timezone.py
│   │   │       ├── verify_email_address.py
│   │   │       ├── voice_endpoint.py
│   │   │       └── wake_word_endpoint.py
│   │   ├── pyproject.toml
│   │   ├── tests/
│   │   │   └── features/
│   │   │       ├── add_device.feature
│   │   │       ├── agreements.feature
│   │   │       ├── authentication.feature
│   │   │       ├── environment.py
│   │   │       ├── pantacor_update.feature
│   │   │       ├── profile.feature
│   │   │       ├── remove_account.feature
│   │   │       └── steps/
│   │   │           ├── add_device.py
│   │   │           ├── agreements.py
│   │   │           ├── authentication.py
│   │   │           ├── common.py
│   │   │           ├── pantacor_update.py
│   │   │           ├── profile.py
│   │   │           └── remove_account.py
│   │   └── uwsgi.ini
│   ├── market/
│   │   ├── market_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── available_skills.py
│   │   │       ├── skill_detail.py
│   │   │       ├── skill_install.py
│   │   │       └── skill_install_status.py
│   │   ├── pyproject.toml
│   │   ├── swagger.yaml
│   │   └── uwsgi.ini
│   ├── precise/
│   │   ├── precise_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── audio_file.py
│   │   │       ├── designation.py
│   │   │       └── tag.py
│   │   ├── pyproject.toml
│   │   └── uwsgi.ini
│   ├── public/
│   │   ├── __init__.py
│   │   ├── public_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── audio_transcription.py
│   │   │       ├── device.py
│   │   │       ├── device_activate.py
│   │   │       ├── device_code.py
│   │   │       ├── device_email.py
│   │   │       ├── device_location.py
│   │   │       ├── device_metrics.py
│   │   │       ├── device_oauth.py
│   │   │       ├── device_pantacor.py
│   │   │       ├── device_refresh_token.py
│   │   │       ├── device_setting.py
│   │   │       ├── device_skill.py
│   │   │       ├── device_skill_manifest.py
│   │   │       ├── device_skill_settings.py
│   │   │       ├── device_subscription.py
│   │   │       ├── geolocation.py
│   │   │       ├── google_stt.py
│   │   │       ├── oauth_callback.py
│   │   │       ├── open_weather_map.py
│   │   │       ├── premium_voice.py
│   │   │       ├── stripe_webhook.py
│   │   │       ├── wake_word_file.py
│   │   │       ├── wolfram_alpha.py
│   │   │       ├── wolfram_alpha_simple.py
│   │   │       ├── wolfram_alpha_spoken.py
│   │   │       └── wolfram_alpha_v2.py
│   │   ├── pyproject.toml
│   │   ├── tests/
│   │   │   └── features/
│   │   │       ├── device_email.feature
│   │   │       ├── device_location.feature
│   │   │       ├── device_metrics.feature
│   │   │       ├── device_pairing.feature
│   │   │       ├── device_refresh_token.feature
│   │   │       ├── device_skill_manifest.feature
│   │   │       ├── device_skill_settings.feature
│   │   │       ├── device_subscription.feature
│   │   │       ├── environment.py
│   │   │       ├── get_device.feature
│   │   │       ├── get_device_settings.feature
│   │   │       ├── steps/
│   │   │       │   ├── common.py
│   │   │       │   ├── device_email.py
│   │   │       │   ├── device_location.py
│   │   │       │   ├── device_metrics.py
│   │   │       │   ├── device_pairing.py
│   │   │       │   ├── device_refresh_token.py
│   │   │       │   ├── device_skill_manifest.py
│   │   │       │   ├── device_skill_settings.py
│   │   │       │   ├── get_device.py
│   │   │       │   ├── get_device_settings.py
│   │   │       │   ├── get_device_subscription.py
│   │   │       │   ├── resources/
│   │   │       │   │   └── test_stt.flac
│   │   │       │   ├── transcribe_audio.py
│   │   │       │   ├── wake_word_file.py
│   │   │       │   └── wolfram_alpha.py
│   │   │       ├── transcribe_audio.feature
│   │   │       ├── wake_word_file_upload.feature
│   │   │       └── wolfram_alpha.feature
│   │   └── uwsgi.ini
│   └── sso/
│       ├── Dockerfile
│       ├── pyproject.toml
│       ├── sso_api/
│       │   ├── __init__.py
│       │   ├── api.py
│       │   └── endpoints/
│       │       ├── __init__.py
│       │       ├── authenticate_internal.py
│       │       ├── github_token.py
│       │       ├── logout.py
│       │       ├── password_change.py
│       │       ├── password_reset.py
│       │       ├── validate_federated.py
│       │       └── validate_token.py
│       ├── tests/
│       │   └── features/
│       │       ├── add_account.feature
│       │       ├── agreements.feature
│       │       ├── environment.py
│       │       ├── federated_login.feature
│       │       ├── internal_login.feature
│       │       ├── logout.feature
│       │       ├── password_change.feature
│       │       └── steps/
│       │           ├── add_account.py
│       │           ├── agreements.py
│       │           ├── common.py
│       │           ├── login.py
│       │           ├── logout.py
│       │           └── password_change.py
│       └── uwsgi.ini
├── batch/
│   ├── job_scheduler/
│   │   ├── __init__.py
│   │   └── jobs.py
│   ├── pyproject.toml
│   └── script/
│       ├── __init__.py
│       ├── daily_report.py
│       ├── delete_wake_word_files.py
│       ├── designate_wake_word_files.py
│       ├── load_skill_display_data.py
│       ├── move_wake_word_files.py
│       ├── parse_core_metrics.py
│       ├── partition_api_metrics.py
│       ├── test_scheduler.py
│       └── update_device_last_contact.py
├── db/
│   ├── mycroft/
│   │   ├── account_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── data/
│   │   │   │   └── membership.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account.sql
│   │   │       ├── account_agreement.sql
│   │   │       ├── account_membership.sql
│   │   │       ├── agreement.sql
│   │   │       └── membership.sql
│   │   ├── create_extensions.sql
│   │   ├── create_mycroft_db.sql
│   │   ├── create_roles.sql
│   │   ├── create_template_db.sql
│   │   ├── device_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── data/
│   │   │   │   └── text_to_speech.sql
│   │   │   ├── get_device_defaults_for_city.sql
│   │   │   ├── get_device_geographies_for_city.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account_defaults.sql
│   │   │       ├── account_preferences.sql
│   │   │       ├── category.sql
│   │   │       ├── device.sql
│   │   │       ├── device_skill.sql
│   │   │       ├── geography.sql
│   │   │       ├── pantacor_config.sql
│   │   │       ├── skill_setting.sql
│   │   │       ├── text_to_speech.sql
│   │   │       ├── wake_word.sql
│   │   │       └── wake_word_settings.sql
│   │   ├── drop_extensions.sql
│   │   ├── drop_mycroft_db.sql
│   │   ├── drop_roles.sql
│   │   ├── drop_template_db.sql
│   │   ├── geography_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── delete_duplicate_cities.sql
│   │   │   ├── get_duplicated_cities.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── city.sql
│   │   │       ├── country.sql
│   │   │       ├── region.sql
│   │   │       └── timezone.sql
│   │   ├── metric_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account_activity.sql
│   │   │       ├── api.sql
│   │   │       ├── api_history.sql
│   │   │       ├── core.sql
│   │   │       ├── core_interaction.sql
│   │   │       ├── job.sql
│   │   │       ├── stt_engine.sql
│   │   │       └── stt_transcription.sql
│   │   ├── skill_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── display.sql
│   │   │       ├── oauth_credential.sql
│   │   │       ├── oauth_token.sql
│   │   │       ├── settings_display.sql
│   │   │       └── skill.sql
│   │   ├── tagging_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── file_location.sql
│   │   │       ├── session.sql
│   │   │       ├── tag.sql
│   │   │       ├── tag_value.sql
│   │   │       ├── tagger.sql
│   │   │       ├── wake_word_file.sql
│   │   │       ├── wake_word_file_designation.sql
│   │   │       └── wake_word_file_tag.sql
│   │   ├── types/
│   │   │   ├── agreement_enum.sql
│   │   │   ├── cateogory_enum.sql
│   │   │   ├── core_version_enum.sql
│   │   │   ├── date_format_enum.sql
│   │   │   ├── measurement_system_enum.sql
│   │   │   ├── membership_type_enum.sql
│   │   │   ├── payment_method_enum.sql
│   │   │   ├── tagger_type_enum.sql
│   │   │   ├── tagging_file_origin_enum.sql
│   │   │   ├── tagging_file_status_enum.sql
│   │   │   ├── time_format_enum.sql
│   │   │   └── tts_engine_enum.sql
│   │   ├── versions/
│   │   │   └── 2020.9.1.sql
│   │   └── wake_word_schema/
│   │       ├── create_schema.sql
│   │       ├── grants.sql
│   │       └── tables/
│   │           ├── pocketsphinx_settings.sql
│   │           └── wake_word.sql
│   ├── pyproject.toml
│   └── scripts/
│       ├── __init__.py
│       ├── bootstrap_mycroft_db.py
│       ├── neo4j-postgres.py
│       ├── queries.cypher
│       └── remove_duplicate_cities.py
└── shared/
    ├── Dockerfile
    ├── MANIFEST.in
    ├── pyproject.toml
    ├── selene/
    │   ├── __init__.py
    │   ├── api/
    │   │   ├── __init__.py
    │   │   ├── base_config.py
    │   │   ├── base_endpoint.py
    │   │   ├── blueprint.py
    │   │   ├── endpoints/
    │   │   │   ├── __init__.py
    │   │   │   ├── account.py
    │   │   │   ├── agreements.py
    │   │   │   ├── password_change.py
    │   │   │   └── validate_email.py
    │   │   ├── etag.py
    │   │   ├── pantacor.py
    │   │   ├── public_endpoint.py
    │   │   └── response.py
    │   ├── batch/
    │   │   ├── __init__.py
    │   │   └── base.py
    │   ├── data/
    │   │   ├── __init__.py
    │   │   ├── account/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── account.py
    │   │   │   │   ├── agreement.py
    │   │   │   │   ├── membership.py
    │   │   │   │   └── skill.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── account.py
    │   │   │       ├── agreement.py
    │   │   │       ├── membership.py
    │   │   │       ├── skill.py
    │   │   │       └── sql/
    │   │   │           ├── add_account.sql
    │   │   │           ├── add_account_agreement.sql
    │   │   │           ├── add_account_membership.sql
    │   │   │           ├── add_agreement.sql
    │   │   │           ├── add_membership.sql
    │   │   │           ├── change_email_address.sql
    │   │   │           ├── change_password.sql
    │   │   │           ├── daily_report.sql
    │   │   │           ├── delete_agreement.sql
    │   │   │           ├── delete_membership.sql
    │   │   │           ├── end_membership.sql
    │   │   │           ├── expire_account_agreement.sql
    │   │   │           ├── expire_agreement.sql
    │   │   │           ├── get_account.sql
    │   │   │           ├── get_account_by_device_id.sql
    │   │   │           ├── get_account_skills.sql
    │   │   │           ├── get_active_membership_by_account_id.sql
    │   │   │           ├── get_active_membership_by_payment_account_id.sql
    │   │   │           ├── get_agreement_content_id.sql
    │   │   │           ├── get_current_agreements.sql
    │   │   │           ├── get_membership_by_type.sql
    │   │   │           ├── get_membership_types.sql
    │   │   │           ├── remove_account.sql
    │   │   │           ├── update_last_activity_ts.sql
    │   │   │           └── update_username.sql
    │   │   ├── device/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── default.py
    │   │   │   │   ├── device.py
    │   │   │   │   ├── device_skill.py
    │   │   │   │   ├── geography.py
    │   │   │   │   ├── preference.py
    │   │   │   │   └── text_to_speech.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── default.py
    │   │   │       ├── device.py
    │   │   │       ├── device_skill.py
    │   │   │       ├── geography.py
    │   │   │       ├── preference.py
    │   │   │       ├── setting.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_device.sql
    │   │   │       │   ├── add_geography.sql
    │   │   │       │   ├── add_manifest_skill.sql
    │   │   │       │   ├── add_text_to_speech.sql
    │   │   │       │   ├── delete_device_skill.sql
    │   │   │       │   ├── get_account_defaults.sql
    │   │   │       │   ├── get_account_device_count.sql
    │   │   │       │   ├── get_account_geographies.sql
    │   │   │       │   ├── get_account_preferences.sql
    │   │   │       │   ├── get_all_device_ids.sql
    │   │   │       │   ├── get_device_by_id.sql
    │   │   │       │   ├── get_device_settings_by_device_id.sql
    │   │   │       │   ├── get_device_skill_manifest.sql
    │   │   │       │   ├── get_devices_by_account_id.sql
    │   │   │       │   ├── get_location_by_device_id.sql
    │   │   │       │   ├── get_open_dataset_agreement_by_device_id.sql
    │   │   │       │   ├── get_settings_display_usage.sql
    │   │   │       │   ├── get_skill_manifest_for_account.sql
    │   │   │       │   ├── get_skill_settings_for_account.sql
    │   │   │       │   ├── get_skill_settings_for_device.sql
    │   │   │       │   ├── get_voices.sql
    │   │   │       │   ├── remove_device.sql
    │   │   │       │   ├── remove_manifest_skill.sql
    │   │   │       │   ├── remove_text_to_speech.sql
    │   │   │       │   ├── update_device_from_account.sql
    │   │   │       │   ├── update_device_from_core.sql
    │   │   │       │   ├── update_device_skill_settings.sql
    │   │   │       │   ├── update_last_contact_ts.sql
    │   │   │       │   ├── update_pantacor_config.sql
    │   │   │       │   ├── update_skill_manifest.sql
    │   │   │       │   ├── update_skill_settings.sql
    │   │   │       │   ├── upsert_defaults.sql
    │   │   │       │   ├── upsert_device_skill_settings.sql
    │   │   │       │   ├── upsert_pantacor_config.sql
    │   │   │       │   └── upsert_preferences.sql
    │   │   │       └── text_to_speech.py
    │   │   ├── geography/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── city.py
    │   │   │   │   ├── country.py
    │   │   │   │   ├── region.py
    │   │   │   │   └── timezone.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── city.py
    │   │   │       ├── country.py
    │   │   │       ├── region.py
    │   │   │       ├── sql/
    │   │   │       │   ├── get_biggest_city_in_country.sql
    │   │   │       │   ├── get_biggest_city_in_region.sql
    │   │   │       │   ├── get_cities_by_region.sql
    │   │   │       │   ├── get_countries.sql
    │   │   │       │   ├── get_geographic_location_by_city.sql
    │   │   │       │   ├── get_regions_by_country.sql
    │   │   │       │   └── get_timezones_by_country.sql
    │   │   │       └── timezone.py
    │   │   ├── metric/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── account_activity.py
    │   │   │   │   ├── api.py
    │   │   │   │   ├── core.py
    │   │   │   │   ├── job.py
    │   │   │   │   └── stt.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── account_activity.py
    │   │   │       ├── api.py
    │   │   │       ├── core.py
    │   │   │       ├── job.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_account_activity.sql
    │   │   │       │   ├── add_api_metric.sql
    │   │   │       │   ├── add_core_interaction.sql
    │   │   │       │   ├── add_core_metric.sql
    │   │   │       │   ├── add_job_metric.sql
    │   │   │       │   ├── add_tts_transcription_metric.sql
    │   │   │       │   ├── create_api_metric_partition.sql
    │   │   │       │   ├── create_api_metric_partition_index.sql
    │   │   │       │   ├── delete_account_activity_date.sql
    │   │   │       │   ├── delete_api_metrics_by_date.sql
    │   │   │       │   ├── delete_stt_transcription_by_date.sql
    │   │   │       │   ├── get_account_activity_by_date.sql
    │   │   │       │   ├── get_api_metrics_for_date.sql
    │   │   │       │   ├── get_core_metric_by_device.sql
    │   │   │       │   ├── get_core_timing_metrics_by_date.sql
    │   │   │       │   ├── get_tts_transcription_by_account.sql
    │   │   │       │   ├── increment_accounts_added.sql
    │   │   │       │   ├── increment_accounts_deleted.sql
    │   │   │       │   ├── increment_activity.sql
    │   │   │       │   ├── increment_members_added.sql
    │   │   │       │   ├── increment_members_expired.sql
    │   │   │       │   ├── increment_open_dataset_added.sql
    │   │   │       │   └── increment_open_dataset_deleted.sql
    │   │   │       └── stt.py
    │   │   ├── repository_base.py
    │   │   ├── skill/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── display.py
    │   │   │   │   ├── skill.py
    │   │   │   │   └── skill_setting.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── display.py
    │   │   │       ├── setting.py
    │   │   │       ├── settings_display.py
    │   │   │       ├── skill.py
    │   │   │       └── sql/
    │   │   │           ├── add_device_skill.sql
    │   │   │           ├── add_settings_display.sql
    │   │   │           ├── add_skill.sql
    │   │   │           ├── delete_device_skill.sql
    │   │   │           ├── delete_settings_display.sql
    │   │   │           ├── get_display_data_for_skill.sql
    │   │   │           ├── get_display_data_for_skills.sql
    │   │   │           ├── get_settings_definition_by_gid.sql
    │   │   │           ├── get_settings_display_id.sql
    │   │   │           ├── get_settings_for_skill_family.sql
    │   │   │           ├── get_skill_by_global_id.sql
    │   │   │           ├── get_skill_setting_by_device.sql
    │   │   │           ├── get_skills_for_account.sql
    │   │   │           ├── remove_skill_by_gid.sql
    │   │   │           ├── update_device_skill_settings.sql
    │   │   │           └── upsert_skill_display_data.sql
    │   │   ├── tagging/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── file_designation.py
    │   │   │   │   ├── file_location.py
    │   │   │   │   ├── file_tag.py
    │   │   │   │   ├── tag.py
    │   │   │   │   ├── tag_value.py
    │   │   │   │   ├── tagger.py
    │   │   │   │   └── wake_word_file.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── file_designation.py
    │   │   │       ├── file_location.py
    │   │   │       ├── file_tag.py
    │   │   │       ├── session.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_file_location.sql
    │   │   │       │   ├── add_session.sql
    │   │   │       │   ├── add_tagger.sql
    │   │   │       │   ├── add_tagging_session.sql
    │   │   │       │   ├── add_wake_word_file.sql
    │   │   │       │   ├── add_wake_word_file_designation.sql
    │   │   │       │   ├── add_wake_word_file_tag.sql
    │   │   │       │   ├── change_account_file_status.sql
    │   │   │       │   ├── change_file_location.sql
    │   │   │       │   ├── change_file_status.sql
    │   │   │       │   ├── get_active_session.sql
    │   │   │       │   ├── get_designation_candidates.sql
    │   │   │       │   ├── get_designations_from_date.sql
    │   │   │       │   ├── get_file_location_id.sql
    │   │   │       │   ├── get_taggable_wake_word_file.sql
    │   │   │       │   ├── get_tagger_by_entity.sql
    │   │   │       │   ├── get_tags.sql
    │   │   │       │   ├── get_wake_word_files.sql
    │   │   │       │   ├── remove_file_location.sql
    │   │   │       │   ├── remove_wake_word_file.sql
    │   │   │       │   └── update_session_end_ts.sql
    │   │   │       ├── tag.py
    │   │   │       ├── tagger.py
    │   │   │       └── wake_word_file.py
    │   │   └── wake_word/
    │   │       ├── __init__.py
    │   │       ├── entity/
    │   │       │   ├── __init__.py
    │   │       │   ├── pocketsphinx_settings.py
    │   │       │   └── wake_word.py
    │   │       └── repository/
    │   │           ├── __init__.py
    │   │           ├── sql/
    │   │           │   ├── add_wake_word.sql
    │   │           │   ├── get_wake_word_id.sql
    │   │           │   ├── get_wake_words_for_web.sql
    │   │           │   └── remove_wake_word.sql
    │   │           └── wake_word.py
    │   ├── testing/
    │   │   ├── __init__.py
    │   │   ├── account.py
    │   │   ├── account_activity.py
    │   │   ├── account_geography.py
    │   │   ├── account_preference.py
    │   │   ├── agreement.py
    │   │   ├── api.py
    │   │   ├── device.py
    │   │   ├── device_skill.py
    │   │   ├── membership.py
    │   │   ├── skill.py
    │   │   ├── tagging.py
    │   │   ├── test_db.py
    │   │   ├── text_to_speech.py
    │   │   └── wake_word.py
    │   └── util/
    │       ├── __init__.py
    │       ├── auth.py
    │       ├── cache.py
    │       ├── db/
    │       │   ├── __init__.py
    │       │   ├── connection.py
    │       │   ├── connection_pool.py
    │       │   ├── cursor.py
    │       │   └── transaction.py
    │       ├── email/
    │       │   ├── __init__.py
    │       │   ├── email.py
    │       │   └── templates/
    │       │       ├── account_not_found.html
    │       │       ├── base.html
    │       │       ├── email_change.html
    │       │       ├── email_verification.html
    │       │       ├── metrics.html
    │       │       ├── password_change.html
    │       │       └── reset_password.html
    │       ├── exceptions.py
    │       ├── github.py
    │       ├── log.py
    │       ├── payment/
    │       │   ├── __init__.py
    │       │   └── stripe.py
    │       └── ssh/
    │           ├── __init__.py
    │           ├── sftp.py
    │           └── ssh.py
    └── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{py}]
charset = utf-8

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

================================================
FILE: .github/CONTRIBUTING.md
================================================
# How to contribute

So you want to contribute to Mycroft?
This should be as easy as possible for you but there are a few things to consider when contributing.
The following guidelines for contribution should be followed if you want to submit a pull request.

## How to prepare

* You need a [GitHub account](https://github.com/signup/free)
* Submit an [issue ticket](https://github.com/MycroftAI/mycroft/issues) for your issue if there is not one yet.
	* Describe the issue and include steps to reproduce if it's a bug.
	* Ensure to mention the earliest version that you know is affected.
* If you are able and want to fix this, fork the repository on GitHub


## Make Changes

  1. [Fork the Project](https://help.github.com/articles/fork-a-repo/)
  2. [Create a new Issue](https://help.github.com/articles/creating-an-issue/)
  3. Create a **feature** or **bugfix** branch based on **dev** with your issue identifier. For example, if your issue identifier is: **issue-123** then you will create either: **feature/issue-123** or **bugfix/issue-123**. Use **feature** prefix for issues related to new functionalities or enhancements and **bugfix** in case of bugs found on the **dev** branch
  4. Make sure you stick to the coding style and OO patterns that are used already.
  5. Document code using [Google-style docstrings](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html).  Our automated documentation tools expect that format.  All functions and class methods that are expected to be called externally should include a docstring.  (And those that aren't [should be prefixed with a single underscore](https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references)).
  6. Make commits in logical units and describe them properly. Use your issue identifier at the very begin of each commit. For instance:
`git commit -m "Issues-123 - Fixing 'A' sound on Spelling Skill"`
  7. Before committing, format your code following the PEP8 rules and organize your imports removing unused libs. To check whether you are following these rules, install pep8 and run `pep8 mycroft test` while in the `mycroft-core` folder. This will check for formatting issues in the `mycroft` and `test` folders.
  8. Once you have committed everything and are done with your branch, you have to rebase your code with **dev**. Do the following steps:
      1. Make sure you do not have any changes left on your branch
      2. Checkout on dev branch and make sure it is up-to-date
      3. Checkout your branch and rebase it with dev
      4. Resolve any conflicts you have
      5. You will have to force your push since the historical base has changed
      6. Suggested steps are:
 ```
git checkout dev
git fetch
git reset --hard origin/dev
git checkout <your_branch_name>
git rebase dev
git push -f
```
  9. If possible, create unit tests for your changes
     * [Unit Tests for most contributions](https://github.com/MycroftAI/mycroft-core/tree/dev/test)
     * [Intent Tests for new skills](https://docs.mycroft.ai/development/creating-a-skill#testing-your-skill)
     * We utilize TRAVIS-CI, which will test each pull request. To test locally you can run: `./start.sh unittest`
  10. Once everything is OK, you can finally [create a Pull Request (PR) on Github](https://help.github.com/articles/using-pull-requests/) in order to be reviewed and merged.

**Note**: Even if you have write access to the master branch, do not work directly on master!

## Submit Changes

* Push your changes to a topic branch in your fork of the repository.
* Open a pull request to the original repository and choose the right original branch you want to patch.
	_Advanced users may install the `hub` gem and use the [`hub pull-request` command](https://github.com/defunkt/hub#git-pull-request)._
* If not done in commit messages (which you really should do) please reference and update your issue with the code changes. But _please do not close the issue yourself_.
* Even if you have write access to the repository, do not directly push or merge pull-requests. Let another team member review your pull request and approve.

# Additional Resources

* [General GitHub documentation](http://help.github.com/)
* [GitHub pull request documentation](https://help.github.com/articles/about-pull-requests/)
* [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) for more details


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
# How to submit an Issue to a Mycroft repository

When submitting an Issue to a Mycroft repository, please follow these guidelines to help us help you. 

## Be clear about the software, hardware and version you are running

For example: 

* I'm running a Mark 1
* With version 0.9.10 of the Mycroft software
* With the standard Wake Word

## Try to provide steps that we can use to replicate the Issue

For example: 

1. Burn the 0.9.10 image to Micro SD card using Etcher
2. Seat the Micro SD card in the RPi 3
3. Boot Picroft
4. Wait 3 minutes
5. The red light will come on indicating that the RPi 3 is overheating
6. Running `htop` via the command line indicates a number of Zombie'd processes

## Be as specific as possible about the expected condition, and the deviation from expected condition. 

This is called _object-deviation format_. Specify the object, then the deviation of the object from an expected condition. 

Example 1: 

* When I say "Hey Mycroft, set your eyes to cadet blue", the eyes turn purple instead of blue. 

Example 2: 

* When I say "Hey Mycroft, what time is it in Paris", the time spoken is out by one hour - it's not observing daylight savings time. 

Example 3: 

* When I run `msm default` on my Mark 1, I receive lots of Git 'locked file' errors on the command line. 

## Provide log files or other output to help us see the error

We will normally require log files or other troubleshooting information to assist you with your Issue. 

This [documentation](https://mycroft.ai/documentation/troubleshooting/) explains how to find log files. 

As of version 0.9.10, the [Support Skill](https://github.com/MycroftAI/skill-support) also helps to automate gathering support information. 

Simply say: 

* "Create a support ticket" _or_
* "You're not working!" _or_
* "Send me debug info"

and the Skill will put together a support package which you can email to us. 

## Upload any files to the Issue that will be useful in helping us to investigate

Please ensure you upload any relevant files - such as screenshots - which will aid us investigating. 


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
(Description of what the PR does, such as fixes # {issue number})

## How to test
(Description of how to validate or test this PR)

## Contributor license agreement signed?
CLA [ ] (Whether you have signed a [CLA - Contributor Licensing Agreement](https://mycroft.ai/cla/)


================================================
FILE: .github/SUPPORT.md
================================================
# How to get support with Mycroft software, hardware and products

There are multiple ways to seek support with Mycroft software, hardware and products.

## Forum

We maintain a [Forum](https://community.mycroft.ai) which is regularly monitored. 
Feel free to post questions, bugs, and requests for assistance in the relevant Forum Topic. 

## Chat

Mycroft staff are regularly available in our [Chat](https://chat.mycroft.ai) platform. 
There are specific rooms available for different projects and products. 

## Contact

You can contact us via [our online form](https://mycroft.ai/contact), or give a call. 

## GitHub

We welcome you raising Issues and Pull Requests on our public GitHub repositories. 
See the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.

## Helping us to help you

Our [documentation](https://mycroft.ai/documentation/troubleshooting/) contains troubleshooting information, and information on log files and other files that we may need to help us help you.




================================================
FILE: .gitignore
================================================
# See http://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
/dist
/tmp
/out-tsc
**/*.egg-info

# dependencies
**/node_modules

# python notebooks
*.ipynb

# IDEs and editors
**/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
__pycache__/

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings

# System Files
.DS_Store
Thumbs.db




================================================
FILE: .pre-commit-config.yaml
================================================
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
    -   id: black


================================================
FILE: AUTHORS
================================================
The Mycroft Server was initially developed by Mycroft AI Inc

It lives on as an open source project with many contributors, a self-updating
list is at: https://github.com/mycroftai/selene-backend/graphs/contributors

================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kathy.reid@mycroft.ai. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: Dockerfile
================================================
# Multi-stage Dockerfile for running Selene APIs or their test suites.
#
# ASSUMPTION:
#   This Dockerfile assumes its resulting containers will run on a Docker network.  A Postgres container named
#   "selene-db" and a Redis container named "selene-cache" also need to be running on this network.  To create the
#   network and the Postgres/Redis containers, use the following commands:
#       docker network create --driver bridge <network name>
#       docker run -d --net <network name> --name selene-cache redis:6
#       docker run -d -e POSTGRES_PASSWORD=selene --net <network name> --name selene-db postgres:10
#   The DB_HOST environment variable is set to the name of the Postgres container and the REDIS_HOST environment
#   variable is set to the name of he Redis container.  When running images created from this Dockerfile, include the
#   "--net <network name>" argument.

# Build steps that apply to all of the selene applications.
FROM python:3.9 as base-build
RUN apt-get update && apt-get -y install gcc git libsndfile-dev
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH ${PATH}:/root/.local/bin
RUN poetry --version
RUN mkdir -p /root/allure /opt/selene/selene-backend /root/code-quality /var/log/mycroft
WORKDIR /opt/selene/selene-backend
ENV DB_HOST selene-db
ENV DB_NAME mycroft
ENV DB_PASSWORD adam
ENV DB_USER selene
ENV JWT_ACCESS_SECRET access-secret
ENV JWT_REFRESH_SECRET refresh-secret
ENV REDIS_HOST selene-cache
ENV REDIS_PORT 6379
ENV SALT testsalt
ENV SELENE_ENVIRONMENT dev

# Put the copy of the shared library code in its own section to avoid reinstalling base software every time
FROM base-build as selene-base
COPY shared shared

# Code quality scripts and user agreements are stored in the MycroftAI/devops repository.  This repository is private.
# builds for publicly available images should not use this build stage.
#
# The GitHub API key is sensitive information and can change depending on who is running the application.
# It is used here to clone the private MycroftAI/devops repository.
FROM selene-base as devops-build
ARG github_api_key
ENV GITHUB_API_KEY=$github_api_key
RUN mkdir -p /opt/mycroft
WORKDIR /opt/mycroft
RUN git clone https://${github_api_key}@github.com/MycroftAI/devops.git
WORKDIR /opt/mycroft/devops/jenkins
RUN poetry install

# Run a linter and code formatter against the API specified in the build argument
FROM devops-build as api-code-check
ARG api_name
WORKDIR /opt/selene/selene-backend
COPY api/${api_name} api/${api_name}
WORKDIR /opt/selene/selene-backend/api/${api_name}
RUN poetry install
ENV PYTHONPATH=$PYTHONPATH:/opt/selene/selene-backend/api/${api_name}
WORKDIR /opt/mycroft/devops/jenkins
ENTRYPOINT ["poetry", "run", "python", "-m", "pipeline.code_check", "--repository", "selene-backend", "--base-dir", "/opt/selene"]

# Bootstrap the Selene database as it will be needed to run any Selene applications.
FROM devops-build as db-bootstrap
ENV POSTGRES_PASSWORD selene
WORKDIR /opt/selene/selene-backend
COPY db db
WORKDIR /opt/selene/selene-backend/db
RUN poetry install
RUN mkdir -p /tmp/selene
ENTRYPOINT ["poetry", "run", "python", "scripts/bootstrap_mycroft_db.py", "--ci"]

# Run the tests defined in the Account API
FROM selene-base as account-api-test
ARG stripe_api_key
ENV ACCOUNT_BASE_URL https://account.mycroft.test
ENV PANTACOR_API_TOKEN pantacor-token
ENV PANTACOR_API_BASE_URL pantacor.test.url
ENV PYTHONPATH=$PYTHONPATH:/opt/selene/selene-backend/api/account
ENV STRIPE_PRIVATE_KEY $stripe_api_key
COPY api/account api/account
WORKDIR /opt/selene/selene-backend/api/account
RUN poetry install
WORKDIR /opt/selene/selene-backend/api/account/tests
ENTRYPOINT ["poetry", "run", "behave", "-f", "allure_behave.formatter:AllureFormatter", "-o", "/root/allure/allure-result"]

# Run the tests defined in the Single Sign On API
FROM selene-base as sso-api-test
ARG github_client_id
ARG github_client_secret
ENV PYTHONPATH=$PYTHONPATH:/opt/selene/selene-backend/api/sso
ENV JWT_RESET_SECRET reset-secret
# The GitHub client ID and secret are sensitive information and can change depending on who is running the application.
# They are used here to facilitate user authentication using a GitHub account.
ENV GITHUB_CLIENT_ID $github_client_id
ENV GITHUB_CLIENT_SECRET $github_client_secret
COPY api/sso api/sso
WORKDIR /opt/selene/selene-backend/api/sso
RUN poetry install
WORKDIR /opt/selene/selene-backend/api/sso/tests
ENTRYPOINT ["poetry", "run", "behave", "-f", "allure_behave.formatter:AllureFormatter", "-o", "/root/allure/allure-result"]

# Run the tests defined in the Public Device API
FROM selene-base as public-api-test
RUN mkdir -p /opt/selene/data
ARG google_stt_key
ARG stt_api_key
ARG wolfram_alpha_key
ENV GOOGLE_APPLICATION_CREDENTIALS="/root/secrets/transcription-test-363101-6532632520e1.json"
ENV GOOGLE_STT_KEY $google_stt_key
ENV PANTACOR_API_TOKEN pantacor-token
ENV PANTACOR_API_BASE_URL pantacor.test.url
ENV PYTHONPATH=$PYTHONPATH:/opt/selene/selene-backend/api/public
ENV GOOGLE_STT_KEY $google_stt_key
ENV SENDGRID_API_KEY test_sendgrid_key
ENV WOLFRAM_ALPHA_KEY $wolfram_alpha_key
ENV WOLFRAM_ALPHA_URL https://api.wolframalpha.com
COPY api/public api/public
WORKDIR /opt/selene/selene-backend/api/public
RUN poetry install
WORKDIR /opt/selene/selene-backend/api/public/tests
ENTRYPOINT ["poetry", "run", "behave", "-f", "allure_behave.formatter:AllureFormatter", "-o", "/root/allure/allure-result"]


================================================
FILE: Jenkinsfile
================================================
pipeline {
    agent any
    options {
        // Running builds concurrently could cause a race condition with
        // building the Docker image.
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '5'))
        ansiColor('xterm')
    }
    environment {
        // Some branches have a "/" in their name (e.g. feature/new-and-cool)
        // Some commands, such as those that deal with directories, don't
        // play nice with this naming convention.  Define an alias for the
        // branch name that can be used in these scenarios.
        BRANCH_ALIAS = sh(
            script: 'echo $BRANCH_NAME | sed -e "s#/#-#g"',
            returnStdout: true
        ).trim()
        DOCKER_BUILDKIT=1
        //spawns GITHUB_USR and GITHUB_PSW environment variables
        GITHUB_API_KEY=credentials('38b2e4a6-167a-40b2-be6f-d69be42c8190')
        GITHUB_CLIENT_ID=credentials('380f58b1-8a33-4a9d-a67b-354a9b0e792e')
        GITHUB_CLIENT_SECRET=credentials('71626c21-de59-4450-bfad-5034fd596fb2')
        GOOGLE_STT_KEY=credentials('287949f8-2ada-4450-8806-1fe2dd8e4c4d')
        STRIPE_KEY=credentials('9980e41f-d418-49af-9d62-341d1246f555')
        WOLFRAM_ALPHA_KEY=credentials('f718e0a1-c19c-4c7f-af88-0689738ccaa1')
    }
    stages {
        stage('Lint & Format') {
            // Run PyLint and Black to check code quality.
            when {
                anyOf {
                    changeRequest target: 'dev'
                    changeRequest target: 'master'
                }
            }
            steps {
                labelledShell label: 'Account API Setup', script: """
                     docker build \
                        --build-arg github_api_key=${GITHUB_API_KEY} \
                        --build-arg api_name=account \
                        --target api-code-check --no-cache \
                        -t selene-linter:${BRANCH_ALIAS} .
                """
                labelledShell label: 'Account API Check', script: """
                    docker run selene-linter:${BRANCH_ALIAS} --poetry-dir api/account --pull-request=${BRANCH_NAME}
                """
                labelledShell label: 'Single Sign On API Setup', script: """
                     docker build \
                        --build-arg github_api_key=${GITHUB_API_KEY} \
                        --build-arg api_name=sso \
                        --target api-code-check --no-cache \
                        -t selene-linter:${BRANCH_ALIAS} .
                """
                labelledShell label: 'Single Sign On API Check', script: """
                    docker run selene-linter:${BRANCH_ALIAS} --poetry-dir api/sso --pull-request=${BRANCH_NAME}
                """
                labelledShell label: 'Public API Setup', script: """
                     docker build \
                        --build-arg github_api_key=${GITHUB_API_KEY} \
                        --build-arg api_name=public \
                        --target api-code-check --no-cache \
                        --label job=${JOB_NAME} \
                        -t selene-linter:${BRANCH_ALIAS} .
                """
                labelledShell label: 'Public API Check', script: """
                    docker run selene-linter:${BRANCH_ALIAS} --poetry-dir api/public --pull-request=${BRANCH_NAME}
                """
            }
        }
        stage('Bootstrap DB') {
            when {
                anyOf {
                    branch 'dev'
                    branch 'master'
                    changeRequest target: 'dev'
                    changeRequest target: 'master'
                }
            }
            steps {
                labelledShell label: 'Building Docker image', script: """
                    docker build \
                        --target db-bootstrap \
                        --build-arg github_api_key=${GITHUB_API_KEY} \
                        --label job=${JOB_NAME} \
                        -t selene-db:${BRANCH_ALIAS} .
                """
                timeout(time: 5, unit: 'MINUTES')
                {
                    labelledShell label: 'Run database bootstrap script', script: """
                        docker run \
                            -v '${HOME}/selene:/tmp/selene' \
                            --net selene-net selene-db:${BRANCH_ALIAS}
                    """
                }
            }
        }
        stage('Account API Tests') {
            when {
                anyOf {
                    branch 'dev'
                    branch 'master'
                    changeRequest target: 'dev'
                    changeRequest target: 'master'
                }
            }
            steps {
                labelledShell label: 'Building Docker image', script: """
                    docker build \
                        --build-arg stripe_api_key=${STRIPE_KEY} \
                        --target account-api-test \
                        --label job=${JOB_NAME} \
                        -t selene-account:${BRANCH_ALIAS} .
                """
                timeout(time: 5, unit: 'MINUTES')
                {
                    sh 'mkdir -p $HOME/selene/$BRANCH_ALIAS/allure'
                    labelledShell label: 'Running behave tests', script: """
                        docker run \
                            --net selene-net \
                            -v '$HOME/selene/$BRANCH_ALIAS/allure/:/root/allure' \
                            --label job=${JOB_NAME} \
                            selene-account:${BRANCH_ALIAS}
                    """
                }
            }
            post {
                always {
                    sh 'docker run \
                        -v "$HOME/selene/$BRANCH_ALIAS/allure:/root/allure" \
                        --entrypoint=/bin/bash \
                        --label build=${JOB_NAME} \
                        selene-account:${BRANCH_ALIAS} \
                        -x -c "chown $(id -u $USER):$(id -g $USER) \
                        -R /root/allure/"'
                }
            }
        }
        stage('Single Sign On API Tests') {
            when {
                anyOf {
                    branch 'dev'
                    branch 'master'
                    changeRequest target: 'dev'
                    changeRequest target: 'master'
                }
            }
            steps {
                labelledShell label: 'Building Docker image', script: """
                    docker build \
                        --build-arg github_client_id=${GITHUB_CLIENT_ID} \
                        --build-arg github_client_secret=${GITHUB_CLIENT_SECRET} \
                        --target sso-api-test \
                        --label job=${JOB_NAME} \
                        -t selene-sso:${BRANCH_ALIAS} .
                """
                timeout(time: 2, unit: 'MINUTES')
                {
                    labelledShell label: 'Running behave tests', script: """
                        docker run \
                            --net selene-net \
                            -v '$HOME/selene/$BRANCH_ALIAS/allure/:/root/allure' \
                            selene-sso:${BRANCH_ALIAS}
                    """
                }
            }
            post {
                always {
                    sh 'docker run \
                        -v "$HOME/selene/$BRANCH_ALIAS/allure:/root/allure" \
                        --entrypoint=/bin/bash \
                        --label build=${JOB_NAME} \
                        selene-sso:${BRANCH_ALIAS} \
                        -x -c "chown $(id -u $USER):$(id -g $USER) \
                        -R /root/allure/"'
                }
            }
        }
        stage('Public Device API Tests') {
            when {
                anyOf {
                    branch 'dev'
                    branch 'master'
                    changeRequest target: 'dev'
                    changeRequest target: 'master'
                }
            }
            steps {
                labelledShell label: 'Building Docker image', script: """
                    docker build \
                        --build-arg wolfram_alpha_key=${WOLFRAM_ALPHA_KEY} \
                        --build-arg google_stt_key=${GOOGLE_STT_KEY} \
                        --target public-api-test \
                        --label job=${JOB_NAME} \
                        -t selene-public:${BRANCH_ALIAS} .
                """
                timeout(time: 2, unit: 'MINUTES')
                {
                    labelledShell label: 'Running behave tests', script: """
                        docker run \
                            --net selene-net \
                            -v '$HOME/selene/$BRANCH_ALIAS/allure/:/root/allure' \
                            -v '$HOME/selene/secrets/:/root/secrets' \
                            selene-public:${BRANCH_ALIAS}
                    """
                }
            }
            post {
                always {
                    sh 'docker run \
                        -v "$HOME/selene/$BRANCH_ALIAS/allure:/root/allure" \
                        --entrypoint=/bin/bash \
                        --label build=${JOB_NAME} \
                        selene-account:${BRANCH_ALIAS} \
                        -x -c "chown $(id -u $USER):$(id -g $USER) \
                        -R /root/allure/"'
                }
            }
        }
    }
    post {
        always {
            sh 'rm -rf allure-result/*'
            sh 'mkdir -p $HOME/selene/$BRANCH_ALIAS/allure/allure-result'
            sh 'mv $HOME/selene/$BRANCH_ALIAS/allure/allure-result allure-result'
            // This directory should now be empty, rmdir will intentionally fail if not.
            sh 'rmdir $HOME/selene/$BRANCH_ALIAS/allure'
            script {
                allure([
                    includeProperties: false,
                    jdk: '',
                    properties: [],
                    reportBuildPolicy: 'ALWAYS',
                    results: [[path: 'allure-result']]
                ])
            }
            sh(
                label: 'Cleanup lingering docker containers and images.',
                script: """
                    docker container prune --force;
                    docker image prune --force;
                """
            )
        }
        success {
            // Docker images should remain upon failure for troubleshooting purposes.  However,
            // if the stage is successful, there is no reason to look back at the Docker image.  In theory
            // broken builds will eventually be fixed so this step should run eventually for every PR
            sh(
                label: 'Delete Docker Image on Success',
                script: '''
                    docker image prune --all --force --filter label=job=${JOB_NAME};
                '''
            )
        }
    }
}


================================================
FILE: LICENSE
================================================
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

================================================
FILE: README.md
================================================
[![License](https://img.shields.io/badge/License-GNU_AGPL%203.0-blue.svg)](LICENSE)
[![CLA](https://img.shields.io/badge/CLA%3F-Required-blue.svg)](https://mycroft.ai/cla)
[![Team](https://img.shields.io/badge/Team-Mycroft_Backend-violetblue.svg)](https://github.com/MycroftAI/contributors/blob/master/team/Mycroft%20Backend.md)
![Status](https://img.shields.io/badge/-Production_ready-green.svg)

[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
[![Join chat](https://img.shields.io/badge/Mattermost-join_chat-brightgreen.svg)](https://chat.mycroft.ai)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)



Selene -- Mycroft's Server Backend
==========

Selene provides the services used by [Mycroft Core](https://github.com/mycroftai/mycroft-core) to manage devices, skills
and settings.  It consists of two repositories.  This one contains Python and SQL representing the database definition,
data access layer, APIs and scripts.  The second repository, [Selene UI](https://github.com/mycroftai/selene-ui),
contains Angular web applications that use the APIs defined in this repository.

There are four APIs defined in this repository, account management, single sign on, skill marketplace and device.
The first three support account.mycroft.ai (aka home.mycroft.ai), sso.mycroft.ai, and market.mycroft.ai, respectively.
The device API is how devices running Mycroft Core communicate with the server. Also included in this repository is
a package containing batch scripts for maintenance and the definition of the database schema.

Each API is designed to run independently of the others. Code common to each of the APIs, such as the Data Access Layer,
can be found in the "shared" directory.  The shared code is an independent Python package required by each of the APIs.
Each API has its own Pipfile so that it can be run in its own virtual environment.

## Installation
The Python code utilizes features introduced in Python 3.7, such as data classes.
[Pipenv](https://pipenv.readthedocs.io/en/latest/) is used for virtual environment and package management.
If you prefer to use pip and pyenv (or virtualenv), you can find the required libraries in the files named "Pipfile".
These instructions will use pipenv commands.

If the Selene applications will be servicing a large number of devices (enterprise usage, for example), it is
recommended that each of the applications run on their own server or virtual machine. This configuration makes it
easier to scale and monitor each application independently.  However, all applications can be run on a single server.
This configuration could be more practical for a household running a handful of devices.

These instructions will assume a multi-server setup for several thousand devices. To run on a single server servicing a
small number of devices, the recommended system requirements are 4 CPU, 8GB RAM and 100GB of disk.  There are a lot of
manual steps in this section that will eventually be replaced with an installation script.

All Selene applications are time zone agnostic.  It is recommended that the time zone on any server running Selene be UTC.

It is recommended to create an application specific user. In these instructions this user will be `mycroft`.

### Postgres DB
* Recommended server configuration: [Ubuntu 18.04 LTS (server install)](https://releases.ubuntu.com/bionic/), 2 CPU, 4GB RAM, 50GB disk.
* Use the package management system to install Python 3.7, Python 3 pip and PostgreSQL 10
```
sudo apt-get install postgresql python3.7 python python3-pip
```
* Set Postgres to start on boot
```
sudo systemctl enable postgresql
```
* Clone the selene-backend and documentation repositories
```
sudo mkdir -p /opt/selene
sudo chown -R mycroft:users /opt/selene
cd /opt/selene
git clone https://github.com/MycroftAI/selene-backend.git
```
* Create the virtual environment for the database code
```
sudo python3.7 -m pip install pipenv
cd /opt/selene/selene-backend/db
pipenv install
```
* Download files from geonames.org used to populate the geography schema tables
```
mkdir -p /opt/selene/data
cd /opt/selene/data
wget http://download.geonames.org/export/dump/countryInfo.txt
wget http://download.geonames.org/export/dump/timeZones.txt
wget http://download.geonames.org/export/dump/admin1CodesASCII.txt
wget http://download.geonames.org/export/dump/cities500.zip
```
* Add environment variables containing these passwords for the bootstrap script
```
export DB_PASSWORD=<selene user password>
export POSTGRES_PASSWORD=<postgres user password>
```
* Generate secure passwords for the postgres user and selene user on the database
```
sudo -u postgres psql -c "ALTER USER postgres PASSWORD '$POSTGRES_PASSWORD'"
sudo -u postgres psql -c "CREATE ROLE selene WITH LOGIN ENCRYPTED PASSWORD '$DB_PASSWORD'"
```
* Run the bootstrap script
```
cd /opt/selene/selene-backend/db/scripts
pipenv run python bootstrap_mycroft_db.py
```
  * Note: if you get an authentication error you can temporarily edit `/etc/postgresql/<version>/main/pg_hba.conf` replacing the following lines:
  ```
  # "local" is for Unix domain socket connections only
  local   all             all                                     trust
  # IPv4 local connections:
  host    all             all             127.0.0.1/32            trust
  ```
* By default, Postgres only listens on localhost.  This will not do for a multi-server setup.  Change the
`listen_addresses` value in the `posgresql.conf` file to the private IP of the database server.  This file is owned by
the `postgres` user so use the following command to edit it (substituting vi for your favorite editor)
```
sudo -u postgres vi /etc/postgres/10/main/postgresql.conf
```
* By default, Postgres only allows connections from localhost.  This will not do for a multi-server setup either.  Add
an entry to the `pg_hba.conf` file for each server that needs to access this database.  This file is also owned by
the `postgres` user so use the following command to edit it (substituting vi for your favorite editor)
```
sudo -u postgres vi /etc/postgres/10/main/pg_hba.conf
```
* Instructions on how to update the `pg_hba.conf` file can be found in
[Postgres' documentation](https://www.postgresql.org/docs/10/auth-pg-hba-conf.html).  Below is an example for reference.
```
# IPv4 Selene connections
host    mycroft         selene          <private IP address>/32          md5
```
* Restart Postgres for the `postgres.conf` and `pg_hba.conf` changes to take effect.
```
sudo systemctl restart postgresql
```

### Redis DB

* Recommended server configuration: Ubuntu 18.04 LTS, 1 CPU, 1GB RAM, 5GB disk.
So as to not reinvent the wheel, here are some easy-to-follow instructions for
[installing Redis on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04).
* By default, Redis only listens on local host. For multi-server setups, one additional step is to change the "bind" variable in `/etc/redis/redis.conf` to be the private IP of the Redis host.

### APIs

The majority of the setup for each API is the same.  This section defines the steps common to all APIs. Steps specific
to each API will be defined in their respective sections.
* Add an application user to the VM. Either give this user sudo privileges or execute the sudo commands below as a user
with sudo privileges.  These instructions will assume a user name of "mycroft"
* Use the package management system to install Python 3.7, Python 3 pip and Python 3.7 Developer Tools
```
sudo apt install python3.7 python3-pip python3.7-dev
sudo python3.7 -m pip install pipenv
```
* Setup the Backend Application Directory
```
sudo mkdir -p /opt/selene
sudo chown -R mycroft:users /opt/selene
```
* Setup the Log Directory
```
sudo mkdir -p /var/log/mycroft
sudo chown -R mycroft:users /var/log/mycroft
```
* Clone the Selene Backend Repository
```
cd /opt/selene
git clone https://github.com/MycroftAI/selene-backend.git
```
* If running in a test environment, be sure to checkout the "test" branch of the repository

#### Single Sign On API
Recommended server configuration: Ubuntu 18.04 LTS, 1 CPU, 1GB RAM, 5GB disk
* Create the virtual environment and install the requirements for the application
```
cd /opt/selene/selene-backend/api/sso
pipenv install
```

#### Account API
* Recommended server configuration: Ubuntu 18.04 LTS, 1 CPU, 1GB RAM, 5GB disk
* Create the virtual environment and install the requirements for the application
```
cd /opt/selene/selene-backend/api/account
pipenv install
```

#### Marketplace API
* Recommended server configuration: Ubuntu 18.04 LTS, 1 CPU, 1GB RAM, 10GB disk
* Create the virtual environment and install the requirements for the application
```
cd /opt/selene/selene-backend/api/market
pipenv install
```

#### Device API
* Recommended server configuration: Ubuntu 18.04 LTS, 2 CPU, 2GB RAM, 50GB disk
* Create the virtual environment and install the requirements for the application
```
cd /opt/selene/selene-backend/api/public
pipenv install
```

#### Precise API
* Recommended server configuration: Ubuntu 18.04 LTS, 1 CPU, 1GB RAM, 5GB disk
* Create the virtual environment and install the requirements for the application
```
cd /opt/selene/selene-backend/api/precise
pipenv install
```
### Running the APIs
Each API is configured to run on port 5000.  This is not a problem if each is running in its own VM but will be an
issue if all APIs are running on the same server, or if port 5000 is already in use.  To address these scenarios,
change the port numbering in the uwsgi.ini file for each API.

#### Single Sign On API
* The SSO application uses three JWTs for authentication. First is an access key, which is required to authenticate a
user for API calls.  Second is a refresh key that automatically refreshes the access key when it expires.  Third is a
reset key, which is used in a password reset scenario.  Generate a secret key for each JWT.
* Any data that can identify a user is encrypted.  Generate a salt that will be used with the encryption algorithm.
* Access to the Github API is required to support logging in with your Github account.  Details can be found
[here](https://developer.github.com/v3/guides/basics-of-authentication/).
* The password reset functionality sends an email to the user with a link to reset their password.  Selene uses
SendGrid to send these emails so a SendGrid account and API key are required.
* Define a systemd service to run the API.  The service defines environment variables that use the secret and API keys
generated in previous steps.
```
sudo vim /etc/systemd/system/sso_api.service
```
```
[Unit]
Description=Mycroft Single Sign On Api
After=network.target

[Service]
User=mycroft
Group=www-data
Restart=always
Type=simple
WorkingDirectory=/opt/selene/selene-backend/api/sso
ExecStart=/usr/local/bin/pipenv run uwsgi --ini uwsgi.ini
Environment=DB_HOST=<IP address or name of database host>
Environment=DB_NAME=mycroft
Environment=DB_PASSWORD=<selene database user password>
Environment=DB_PORT=5432
Environment=DB_USER=selene
Environment=GITHUB_CLIENT_ID=<github client id>
Environment=GITHUB_CLIENT_SECRET=<github client secret>
Environment=JWT_ACCESS_SECRET=<access secret>
Environment=JWT_REFRESH_SECRET=<refresh secret>
Environment=JWT_RESET_SECRET=<reset secret>
Environment=SALT=<salt value>
Environment=SELENE_ENVIRONMENT=<test/prod>
Environment=SENDGRID_API_KEY=<sendgrid API key>
Environment=SSO_BASE_URL=<base url for single sign on application>

[Install]
WantedBy=multi-user.target
```
* Start the sso_api service and set it to start on boot
```
sudo systemctl start sso_api.service
sudo systemctl enable sso_api.service
```

#### Account API
* The account API uses the same authentication mechanism as the single sign on API.  The JWT_ACCESS_SECRET,
JWT_REFRESH_SECRET and SALT environment variables must be the same values as those on the single sign on API.
* This application uses the Redis database so the service needs to know where it resides.
* Define a systemd service to run the API.  The service defines environment variables that use the secret and API keys
generated in previous steps.
```
sudo vim /etc/systemd/system/account_api.service
```
```
[Unit]
Description=Mycroft Account API
After=network.target

[Service]
User=mycroft
Group=www-data
Restart=always
Type=simple
WorkingDirectory=/opt/selene/selene-backend/api/account
ExecStart=/usr/local/bin/pipenv run uwsgi --ini uwsgi.ini
Environment=DB_HOST=<db host IP address or name>
Environment=DB_NAME=mycroft
Environment=DB_PASSWORD=<selene user database password>
Environment=DB_PORT=5432
Environment=DB_USER=selene
Environment=JWT_ACCESS_SECRET=<same as value for single sign on>
Environment=JWT_REFRESH_SECRET=<same as value for single sign on>
Environment=OAUTH_BASE_URL=<url for oauth service>
Environment=REDIS_HOST=<IP address or name of redis host>
Environment=REDIS_PORT=6379
Environment=SELENE_ENVIRONMENT=<test/prod>
Environment=SALT=<same as value for single sign on>

[Install]
WantedBy=multi-user.target
```
* Start the account_api service and set it to start on boot
```
sudo systemctl start account_api.service
sudo systemctl enable account_api.service
```

#### Marketplace API
* The marketplace API uses the same authentication mechanism as the single sign on API.  The JWT_ACCESS_SECRET,
JWT_REFRESH_SECRET and SALT environment variables must be the same values as those on the single sign on API.
* This application uses the Redis database so the service needs to know where it resides.
* Define a systemd service to run the API.  The service defines environment variables that use the secret and API keys
generated in previous steps.
```
sudo vim /etc/systemd/system/market_api.service
```
```
[Unit]
Description=Mycroft Marketplace API
After=network.target

[Service]
User=mycroft
Group=www-data
Restart=always
Type=simple
WorkingDirectory=/opt/selene/selene-backend/api/market
ExecStart=/usr/local/bin/pipenv run uwsgi --ini uwsgi.ini
Environment=DB_HOST=<db host IP address or name>
Environment=DB_NAME=mycroft
Environment=DB_PASSWORD=<selene user database password>
Environment=DB_PORT=5432
Environment=DB_USER=selene
Environment=JWT_ACCESS_SECRET=<same as value for single sign on>
Environment=JWT_REFRESH_SECRET=<same as value for single sign on>
Environment=OAUTH_BASE_URL=<url for oauth service>
Environment=REDIS_HOST=<IP address or name of redis host>
Environment=REDIS_PORT=6379
Environment=SELENE_ENVIRONMENT=<test/prod>
Environment=SALT=<same as value for single sign on>

[Install]
WantedBy=multi-user.target
```
* Start the market_api service and set it to start on boot
```
sudo systemctl start market_api.service
sudo systemctl enable market_api.service
```
* The marketplace API assumes that the skills it supplies to the web application are in the Postgres database. To get
them there, a script needs to be run to download them from Github.  The script requires the GITHUB_USER, GITHUB_PASSWORD,
DB_HOST, DB_NAME, DB_USER and DB_PASSWORD environment variables to run.  Use the same values as those in the service
definition files.
```
cd /opt/selene/selene-backend/batch
pipenv install
pipenv run python load_skill_display_data.py --core-version <specify core version, e.g. 19.02>
```

#### Device API
* The device API uses the same authentication mechanism as the single sign on API.  The JWT_ACCESS_SECRET,
JWT_REFRESH_SECRET and SALT environment variables must be the same values as those on the single sign on API.
* This application uses the Redis database so the service needs to know where it resides.
* The weather skill requires a key to the Open Weather Map API
* The speech to text engine requires a key to Google's STT API.
* The Wolfram Alpha skill requires an API key to the Wolfram Alpha API
* Define a systemd service to run the API.  The service defines environment variables that use the secret and API keys
generated in previous steps.
```
sudo vim /etc/systemd/system/public_api.service
```
```
[Unit]
Description=Mycroft Public API
After=network.target

[Service]
User=mycroft
Group=www-data
Restart=always
Type=simple
WorkingDirectory=/opt/selene/selene-backend/api/public
ExecStart=/usr/local/bin/pipenv run uwsgi --ini uwsgi.ini
Environment=DB_HOST=<db host IP address or name>
Environment=DB_NAME=mycroft
Environment=DB_PASSWORD=<selene user database password>
Environment=DB_PORT=5432
Environment=DB_USER=selene
Environment=EMAIL_SERVICE_HOST=<email host>
Environment=EMAIL_SERVICE_PORT=<email port>
Environment=EMAIL_SERVICE_USER=<email user>
Environment=EMAIL_SERVICE_PASSWORD=<email password>
Environment=GOOGLE_STT_KEY=<Google STT API key>
Environment=JWT_ACCESS_SECRET=<same as value for single sign on>
Environment=JWT_REFRESH_SECRET=<same as value for single sign on>
Environment=OAUTH_BASE_URL=<url for oauth service>
Environment=OWM_KEY=<Open Weather Map API Key>
Environment=OWM_URL=https://api.openweathermap.org/data/2.5
Environment=REDIS_HOST=<IP address or name of redis host>
Environment=REDIS_PORT=6379
Environment=SELENE_ENVIRONMENT=<test/prod>
Environment=SALT=<same as value for single sign on>
Environment=WOLFRAM_ALPHA_KEY=<Wolfram Alpha API Key
Environment=WOLFRAM_ALPHA_URL=https://api.wolframalpha.com

[Install]
WantedBy=multi-user.target
```
* Start the public_api service and set it to start on boot
```
sudo systemctl start public_api.service
sudo systemctl enable public_api.service
```

### Testing the endpoints

Before we continue, let's make sure that your endpoints are operational - for this we'll use the `public_api` endpoint as an example.

1. As we do not yet have a http router configured, we must change the `uwsgi` configuration for the endpoint we want to test. This is contained in: `/opt/selene/selene-backend/api/public/uwsgi.ini`. Here we want to replace
    ```
    socket = :$PORT
    ```
    with
    ```
    http = :$PORT
    ```
    then restart the service:
    ```
    sudo systemctl restart public_api.service
    ```

2. Check the status of the systemd service:
    ```
    systemctl status public_api.service
    ```
    Should report the service as "active (running)"

3. Send a GET request from a remote device:
    ```
    curl -v http://$IP_ADDRESS:$PORT/code?state=this-is-a-test
    ```
    You can also monitor this from the service logs by running:
    ```
    journalctl -u public_api.service -f
    ```

## Other Considerations
### DNS
There are multiple ways to setup DNS.  This document will not dictate how to do so for Selene.  However, here is an
example, based on how DNS is setup at Mycroft AI...

Each application runs on its own sub-domain.  Assuming a top level domain of "mycroft.ai" the subdomains are:
* account.mycroft.ai
* api.mycroft.ai
* market.mycroft.ai
* sso.mycroft.ai

The APIs that support the web applications are directories within the sub-domain (e.g. account.mycroft.ai/api).  Since
the device API is externally facing, it is versioned.  It's subdirectory must be "v1".

### Reverse Proxy
There are multiple tools available for setting up a reverse proxy that will point your DNS entries to your APIs. As such, the decision on how to set this up will be left to the user.

### SSL
It is recommended that Selene applications be run using HTTPS.  To do this an SSL certificate is necessary.

[Let's Encrypt](https://letsencrypt.org) is a great way to easily set up SSL certificates for free.

## What About the GUI???
Once the database and API setup is complete, the next step is to setup the GUI, The README file for the
[Selene UI](https://github.com/mycroftai/selene-ui) repository contains the instructions for setting up the web
applications.

## Getting Involved

This is an open source project and we would love your help. We have prepared a [contributing](.github/CONTRIBUTING.md)
guide to help you get started.

If this is your first PR or you're not sure where to get started,
say hi in [Mycroft Chat](https://chat.mycroft.ai/) and a team member would be happy to guide you.
Join the [Mycroft Forum](https://community.mycroft.ai/) for questions and answers.


================================================
FILE: api/account/account_api/__init__.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


================================================
FILE: api/account/account_api/api.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""Entry point for the API that supports the Mycroft Marketplace."""
from flask import Flask

from selene.api import get_base_config, selene_api, SeleneResponse
from selene.api.endpoints import (
    AccountEndpoint,
    AgreementsEndpoint,
    ValidateEmailEndpoint,
)
from selene.util.cache import SeleneCache
from selene.util.log import configure_selene_logger
from .endpoints import (
    AccountDefaultsEndpoint,
    CityEndpoint,
    CountryEndpoint,
    EmailAddressChangeEndpoint,
    DeviceEndpoint,
    DeviceCountEndpoint,
    GeographyEndpoint,
    MembershipEndpoint,
    RegionEndpoint,
    PairingCodeEndpoint,
    PasswordChangeEndpoint,
    PreferencesEndpoint,
    SkillsEndpoint,
    SkillOauthEndpoint,
    SkillSettingsEndpoint,
    SoftwareUpdateEndpoint,
    SshKeyValidatorEndpoint,
    TimezoneEndpoint,
    VerifyEmailAddressEndpoint,
    VoiceEndpoint,
    WakeWordEndpoint,
)

configure_selene_logger("account_api")


# Define the Flask application
acct = Flask(__name__)
acct.config.from_object(get_base_config())
acct.response_class = SeleneResponse
acct.register_blueprint(selene_api)
acct.config["SELENE_CACHE"] = SeleneCache()

account_endpoint = AccountEndpoint.as_view("account_endpoint")
acct.add_url_rule(
    "/api/account", view_func=account_endpoint, methods=["GET", "PATCH", "DELETE"]
)

agreements_endpoint = AgreementsEndpoint.as_view("agreements_endpoint")
acct.add_url_rule(
    "/api/agreement/<string:agreement_type>",
    view_func=agreements_endpoint,
    methods=["GET"],
)

city_endpoint = CityEndpoint.as_view("city_endpoint")
acct.add_url_rule("/api/cities", view_func=city_endpoint, methods=["GET"])

country_endpoint = CountryEndpoint.as_view("country_endpoint")
acct.add_url_rule("/api/countries", view_func=country_endpoint, methods=["GET"])

defaults_endpoint = AccountDefaultsEndpoint.as_view("defaults_endpoint")
acct.add_url_rule(
    "/api/defaults", view_func=defaults_endpoint, methods=["GET", "PATCH", "POST"]
)

device_endpoint = DeviceEndpoint.as_view("device_endpoint")
acct.add_url_rule(
    "/api/devices",
    defaults={"device_id": None},
    view_func=device_endpoint,
    methods=["GET"],
)
acct.add_url_rule("/api/devices", view_func=device_endpoint, methods=["POST"])
acct.add_url_rule(
    "/api/devices/<string:device_id>",
    view_func=device_endpoint,
    methods=["DELETE", "GET", "PATCH"],
)

device_count_endpoint = DeviceCountEndpoint.as_view("device_count_endpoint")
acct.add_url_rule("/api/device-count", view_func=device_count_endpoint, methods=["GET"])

change_email_endpoint = EmailAddressChangeEndpoint.as_view("change_email_endpoint")
acct.add_url_rule("/api/change-email", view_func=change_email_endpoint, methods=["PUT"])

change_password_endpoint = PasswordChangeEndpoint.as_view("change_password_endpoint")
acct.add_url_rule(
    "/api/change-password", view_func=change_password_endpoint, methods=["PUT"]
)

geography_endpoint = GeographyEndpoint.as_view("geography_endpoint")
acct.add_url_rule("/api/geographies", view_func=geography_endpoint, methods=["GET"])

membership_endpoint = MembershipEndpoint.as_view("membership_endpoint")
acct.add_url_rule("/api/memberships", view_func=membership_endpoint, methods=["GET"])

pairing_code_endpoint = PairingCodeEndpoint.as_view("pairing_code_endpoint")
acct.add_url_rule(
    "/api/pairing-code/<string:pairing_code>",
    view_func=pairing_code_endpoint,
    methods=["GET"],
)

preferences_endpoint = PreferencesEndpoint.as_view("preferences_endpoint")
acct.add_url_rule(
    "/api/preferences", view_func=preferences_endpoint, methods=["GET", "PATCH", "POST"]
)

region_endpoint = RegionEndpoint.as_view("region_endpoint")
acct.add_url_rule("/api/regions", view_func=region_endpoint, methods=["GET"])

setting_endpoint = SkillSettingsEndpoint.as_view("setting_endpoint")
acct.add_url_rule(
    "/api/skills/<string:skill_family_name>/settings",
    view_func=setting_endpoint,
    methods=["GET", "PUT"],
)

skill_endpoint = SkillsEndpoint.as_view("skill_endpoint")
acct.add_url_rule("/api/skills", view_func=skill_endpoint, methods=["GET"])

skill_oauth_endpoint = SkillOauthEndpoint.as_view("skill_oauth_endpoint")
acct.add_url_rule(
    "/api/skills/oauth/<int:oauth_id>", view_func=skill_oauth_endpoint, methods=["GET"]
)

software_update_endpoint = SoftwareUpdateEndpoint.as_view("software_update_endpoint")
acct.add_url_rule(
    "/api/software-update", view_func=software_update_endpoint, methods=["PATCH"]
)

ssh_key_validation_endpoint = SshKeyValidatorEndpoint.as_view(
    "ssh_key_validation_endpoint"
)
acct.add_url_rule(
    "/api/ssh-key",
    view_func=ssh_key_validation_endpoint,
    methods=["GET"],
)

timezone_endpoint = TimezoneEndpoint.as_view("timezone_endpoint")
acct.add_url_rule("/api/timezones", view_func=timezone_endpoint, methods=["GET"])

validate_email_endpoint = ValidateEmailEndpoint.as_view("validate_email_endpoint")
acct.add_url_rule(
    "/api/validate-email", view_func=validate_email_endpoint, methods=["GET"]
)

verify_email_endpoint = VerifyEmailAddressEndpoint.as_view("verify_email_endpoint")
acct.add_url_rule("/api/verify-email", view_func=verify_email_endpoint, methods=["PUT"])

voice_endpoint = VoiceEndpoint.as_view("voice_endpoint")
acct.add_url_rule("/api/voices", view_func=voice_endpoint, methods=["GET"])

wake_word_endpoint = WakeWordEndpoint.as_view("wake_word_endpoint")
acct.add_url_rule("/api/wake-words", view_func=wake_word_endpoint, methods=["GET"])


================================================
FILE: api/account/account_api/endpoints/__init__.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Public API into the endpoints package."""

from .preferences import PreferencesEndpoint
from .change_email_address import EmailAddressChangeEndpoint
from .change_password import PasswordChangeEndpoint
from .city import CityEndpoint
from .country import CountryEndpoint
from .defaults import AccountDefaultsEndpoint
from .device import DeviceEndpoint
from .device_count import DeviceCountEndpoint
from .geography import GeographyEndpoint
from .membership import MembershipEndpoint
from .pairing_code import PairingCodeEndpoint
from .region import RegionEndpoint
from .skills import SkillsEndpoint
from .skill_oauth import SkillOauthEndpoint
from .skill_settings import SkillSettingsEndpoint
from .software_update import SoftwareUpdateEndpoint
from .ssh_key_validator import SshKeyValidatorEndpoint
from .timezone import TimezoneEndpoint
from .verify_email_address import VerifyEmailAddressEndpoint
from .voice_endpoint import VoiceEndpoint
from .wake_word_endpoint import WakeWordEndpoint


================================================
FILE: api/account/account_api/endpoints/change_email_address.py
================================================
#  Mycroft Server - Backend
#  Copyright (c) 2022 Mycroft AI Inc
#  SPDX-License-Identifier: 	AGPL-3.0-or-later
#  #
#  This file is part of the Mycroft Server.
#  #
#  The Mycroft Server is free software: you can redistribute it and/or
#  modify it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#  #
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU Affero General Public License for more details.
#  #
#  You should have received a copy of the GNU Affero General Public License
#  along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Defines the password change endpoint for the account API.

This endpoint does not update the email address in the database.  The user needs to
verify the email address is correct before the change is applied.  See the
verify_email_address module in this package for the verification step.
"""
from binascii import a2b_base64, b2a_base64
from http import HTTPStatus
from os import environ

from selene.api import APIError, SeleneEndpoint
from selene.util.email import EmailMessage, SeleneMailer, validate_email_address


class EmailAddressChangeEndpoint(SeleneEndpoint):
    """Adds authentication to the common password changing endpoint."""

    def put(self):
        """Executes an HTTP PUT request."""
        self._authenticate()
        new_email_address = self._validate_request()
        self._send_notification()
        self._send_verification_email(new_email_address)

        return "", HTTPStatus.NO_CONTENT

    def _validate_request(self) -> str:
        """Validates the content of the API request.

        :returns: A validated and normalized email address
        :raises: APIError when email address is invalid
        """
        request_token = self.request.json["token"]
        new_email_address = a2b_base64(request_token).decode()
        normalized_address, error = validate_email_address(new_email_address)
        if error is not None:
            raise APIError(error)

        return normalized_address

    def _send_notification(self):
        """Notifies the current email address' owner of the requested change."""
        _, error = validate_email_address(self.account.email_address)
        if error is None:
            email = EmailMessage(
                recipient=self.account.email_address,
                sender="Mycroft AI<no-reply@mycroft.ai>",
                subject="Email Address Changed",
                template_file_name="email_change.html",
            )
            mailer = SeleneMailer(email)
            mailer.send(using_jinja=True)

    @staticmethod
    def _send_verification_email(new_email_address):
        """Sends an email with a link for email verification to the requested address.

        :param new_email_address: the recipient of the verification email
        """
        base_url = environ["ACCOUNT_BASE_URL"]
        token = b2a_base64(new_email_address.encode(), newline=False).decode()
        url = f"{base_url}/verify-email?token={token}"
        email = EmailMessage(
            recipient=new_email_address,
            sender="Mycroft AI<no-reply@mycroft.ai>",
            subject="Email Change Verification",
            template_file_name="email_verification.html",
            template_variables=dict(email_verification_url=url),
        )
        mailer = SeleneMailer(email)
        mailer.send(using_jinja=True)


================================================
FILE: api/account/account_api/endpoints/change_password.py
================================================
#  Mycroft Server - Backend
#  Copyright (c) 2022 Mycroft AI Inc
#  SPDX-License-Identifier: 	AGPL-3.0-or-later
#  #
#  This file is part of the Mycroft Server.
#  #
#  The Mycroft Server is free software: you can redistribute it and/or
#  modify it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#  #
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU Affero General Public License for more details.
#  #
#  You should have received a copy of the GNU Affero General Public License
#  along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Defines the password change endpoint for the account API."""
from selene.api.endpoints import PasswordChangeEndpoint as CommonPasswordChangeEndpoint
from selene.util.email import EmailMessage, SeleneMailer


class PasswordChangeEndpoint(CommonPasswordChangeEndpoint):
    """Adds authentication to the common password changing endpoint."""

    @property
    def account_id(self):
        return self.account.id

    def _send_email(self):
        email = EmailMessage(
            recipient=self.account.email_address,
            sender="Mycroft AI<no-reply@mycroft.ai>",
            subject="Password Changed",
            template_file_name="password_change.html",
        )
        mailer = SeleneMailer(email)
        mailer.send(using_jinja=True)


================================================
FILE: api/account/account_api/endpoints/city.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Account API endpoint for retrieving city geographical information."""
from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.geography import CityRepository


class CityEndpoint(SeleneEndpoint):
    """Retrieve a city in a region"""

    def get(self):
        """Process an HTTP GET request."""
        region_id = self.request.args["region"]
        city_repository = CityRepository(self.db)
        cities = city_repository.get_cities_by_region(region_id=region_id)

        for city in cities:
            city.longitude = float(city.longitude)
            city.latitude = float(city.latitude)

        return cities, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/country.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.geography import CountryRepository


class CountryEndpoint(SeleneEndpoint):
    def get(self):
        country_repository = CountryRepository(self.db)
        countries = country_repository.get_countries()

        return countries, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/defaults.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Account API endpoint for account defaults."""
from http import HTTPStatus
from flask import json
from schematics import Model
from schematics.types import StringType

from selene.api import SeleneEndpoint
from selene.data.device import DefaultsRepository
from selene.util.log import get_selene_logger

_log = get_selene_logger(__name__)


class DefaultsRequest(Model):
    """Data model of the POST request."""

    city = StringType()
    country = StringType()
    region = StringType()
    timezone = StringType()
    voice = StringType()
    wake_word = StringType()


class AccountDefaultsEndpoint(SeleneEndpoint):
    """Handle account default HTTP requests."""

    def __init__(self):
        super().__init__()
        self.defaults = None

    def get(self):
        """Process a HTTP GET request."""
        self._authenticate()
        self._get_defaults()
        if self.defaults is None:
            response_data = ""
            response_code = HTTPStatus.NO_CONTENT
        else:
            response_data = self.defaults
            response_code = HTTPStatus.OK

        return response_data, response_code

    def _get_defaults(self):
        """Get the account defaults from the database."""
        default_repository = DefaultsRepository(self.db, self.account.id)
        self.defaults = default_repository.get_account_defaults()
        if self.defaults is not None and self.defaults.wake_word.name is not None:
            self.defaults.wake_word.name = self.defaults.wake_word.name.title()

    def post(self):
        """Process a HTTP POST request."""
        self._authenticate()
        defaults = self._validate_request()
        self._upsert_defaults(defaults)

        return "", HTTPStatus.NO_CONTENT

    def patch(self):
        """Process an HTTP PATCH request."""
        self._authenticate()
        defaults = self._validate_request()
        self._upsert_defaults(defaults)

        return "", HTTPStatus.NO_CONTENT

    def _validate_request(self) -> dict:
        """Validate the data on the POST/PATCH request"""
        request_data = json.loads(self.request.data)
        defaults = DefaultsRequest()
        defaults.city = request_data.get("city")
        defaults.country = request_data.get("country")
        defaults.region = request_data.get("region")
        defaults.timezone = request_data.get("timezone")
        defaults.voice = request_data["voice"]
        defaults.wake_word = request_data["wakeWord"]
        defaults.validate()

        return defaults.to_native()

    def _upsert_defaults(self, defaults: dict):
        """Apply the changes in the request to the database."""
        defaults_repository = DefaultsRepository(self.db, self.account.id)
        wake_word_default = defaults.get("wake_word")
        if wake_word_default is not None:
            defaults["wake_word"] = defaults["wake_word"].lower()
        defaults_repository.upsert(defaults)


================================================
FILE: api/account/account_api/endpoints/device.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Account API endpoint for retrieving and maintaining device information."""
from dataclasses import asdict
from datetime import datetime, timedelta
from http import HTTPStatus
from typing import List, Optional

from flask import json
from schematics import Model
from schematics.exceptions import ValidationError
from schematics.types import BooleanType, StringType

from selene.api import SeleneEndpoint
from selene.api.etag import ETagManager
from selene.api.pantacor import get_pantacor_pending_deployment, update_pantacor_config
from selene.api.public_endpoint import delete_device_login
from selene.data.device import Device, DeviceRepository, Geography, GeographyRepository
from selene.util.cache import (
    DEVICE_LAST_CONTACT_KEY,
    DEVICE_PAIRING_CODE_KEY,
    DEVICE_PAIRING_TOKEN_KEY,
    SeleneCache,
)
from selene.util.db import use_transaction
from selene.util.log import get_selene_logger

ONE_DAY = 86400
CONNECTED = "Connected"
DISCONNECTED = "Disconnected"
DORMANT = "Dormant"

_log = get_selene_logger(__name__)


def validate_pairing_code(pairing_code):
    """Ensure the pairing code exists in the cache of valid pairing codes."""
    cache_key = DEVICE_PAIRING_CODE_KEY.format(pairing_code=pairing_code)
    cache = SeleneCache()
    pairing_cache = cache.get(cache_key)

    if pairing_cache is None:
        raise ValidationError("pairing code not found")


class UpdateDeviceRequest(Model):
    """Schematic for a request to update a device."""

    city = StringType(required=True)
    country = StringType(required=True)
    name = StringType(required=True)
    placement = StringType()
    region = StringType(required=True)
    timezone = StringType(required=True)
    wake_word = StringType(required=True, deserialize_from="wakeWord")
    voice = StringType(required=True)
    auto_update = BooleanType(deserialize_from="autoUpdate")
    ssh_public_key = StringType(deserialize_from="sshPublicKey")
    release_channel = StringType(deserialize_from="releaseChannel")


class NewDeviceRequest(UpdateDeviceRequest):
    """Schematic for a request to add a device."""

    pairing_code = StringType(
        required=True,
        deserialize_from="pairingCode",
        validators=[validate_pairing_code],
    )


class DeviceEndpoint(SeleneEndpoint):
    """Retrieve and maintain device information for the Account API"""

    _device_repository = None

    def __init__(self):
        super().__init__()
        self.devices = None
        self.validated_request = None
        self.cache = self.config["SELENE_CACHE"]
        self.etag_manager: ETagManager = ETagManager(self.cache, self.config)
        self.pantacor_channels = dict(
            myc200_dev_test="Development",
            myc200_beta_qa_test="Beta QA",
            myc200_beta="Beta",
            myc200_stable="Stable",
            myc200_lts="LTS",
        )

    @property
    def device_repository(self):
        """Lazily instantiate the device repository."""
        if self._device_repository is None:
            self._device_repository = DeviceRepository(self.db)

        return self._device_repository

    def get(self, device_id: str):
        """Process an HTTP GET request."""
        self._authenticate()
        if device_id is None:
            response_data = self._get_devices()
        else:
            response_data = self._get_device(device_id)

        return response_data, HTTPStatus.OK

    def _get_devices(self) -> List[dict]:
        """Get a list of the devices belonging to the account in the request JWT

        :return: list of devices to be returned to the UI.
        """
        devices = self.device_repository.get_devices_by_account_id(self.account.id)
        response_data = []
        for device in devices:
            response_device = self._format_device_for_response(device)
            response_data.append(response_device)

        return response_data

    def _get_device(self, device_id: str) -> dict:
        """Get the device information for a specific device.

        :param device_id: Identifier of the device to retrieve
        :return: device information to return to the UI
        """
        device = self.device_repository.get_device_by_id(device_id)
        response_data = self._format_device_for_response(device)

        return response_data

    def _format_device_for_response(self, device: Device) -> dict:
        """Convert device object into a response object for this endpoint.

        :param device: the device data retrieved from the database.
        :return: device information formatted for the UI
        """
        pantacor_config = self._format_pantacor_config(device.pantacor_config)
        device_status, disconnect_duration = self._format_device_status(device)
        formatted_device = asdict(device)
        formatted_device["pantacor_config"].update(pantacor_config)
        formatted_device["wake_word"].update(name=device.wake_word.name.title())
        formatted_device.update(
            status=device_status,
            disconnect_duration=disconnect_duration,
            voice=formatted_device.pop("text_to_speech"),
        )

        return formatted_device

    def _format_pantacor_config(self, config) -> dict[str, str]:
        """Converts Pantacor config values in the database into displayable values.

        :param config: Pantacor config database values
        :returns: Pantacor config displayable values
        """
        formatted_config = dict(deployment_id=None)
        manual_update = config.auto_update is not None and not config.auto_update
        if manual_update:
            formatted_config.update(
                deployment_id=get_pantacor_pending_deployment(config.pantacor_id)
            )
        if config.release_channel is not None:
            formatted_config.update(
                release_channel=self.pantacor_channels.get(config.release_channel)
            )

        return formatted_config

    def _format_device_status(self, device: Device) -> tuple[str, Optional[str]]:
        """Determines the status of the device being returned.

        :param device: The device to determine the status of
        :return: status of the device and the duration of disconnect (if applicable)
        """
        last_contact_age = self._get_device_last_contact(device)
        device_status = self._determine_device_status(last_contact_age)
        if device_status == DISCONNECTED:
            disconnect_duration = self._determine_disconnect_duration(last_contact_age)
        else:
            disconnect_duration = None

        return device_status, disconnect_duration

    def _get_device_last_contact(self, device: Device) -> timedelta:
        """Get the last time the device contacted the backend.

        The timestamp returned by this method will be used to determine if a
        device is active or not.

        The device table has a last contacted column but it is only updated
        daily via batch script.  The real-time values are kept in Redis.
        If the Redis query returns nothing, the device hasn't contacted the
        backend yet.  This could be because it was just activated. Give the
        device a couple of minutes to make that first call to the backend.

        :param device: the device data retrieved from the database.
        :return: the timestamp the device was last seen by Selene
        """
        last_contact_ts = self.cache.get(
            DEVICE_LAST_CONTACT_KEY.format(device_id=device.id)
        )
        if last_contact_ts is None:
            if device.last_contact_ts is None:
                last_contact_age = datetime.utcnow() - device.add_ts
            else:
                last_contact_age = datetime.utcnow() - device.last_contact_ts
        else:
            last_contact_ts = last_contact_ts.decode()
            last_contact_ts = datetime.strptime(last_contact_ts, "%Y-%m-%d %H:%M:%S.%f")
            last_contact_age = datetime.utcnow() - last_contact_ts

        return last_contact_age

    @staticmethod
    def _determine_device_status(last_contact_age: timedelta) -> str:
        """Derive device status from the last time device contacted servers.

        :param last_contact_age: amount of time since the device was last seen
        :return: the status of the device
        """
        if last_contact_age <= timedelta(seconds=120):
            device_status = CONNECTED
        elif timedelta(seconds=120) < last_contact_age < timedelta(days=30):
            device_status = DISCONNECTED
        else:
            device_status = DORMANT

        return device_status

    @staticmethod
    def _determine_disconnect_duration(last_contact_age: timedelta) -> str:
        """Derive device status from the last time device contacted servers.

        :param last_contact_age: amount of time since the device was last seen
        :return human readable amount of time since the device was last seen
        """
        disconnect_duration = "unknown"
        days, _ = divmod(last_contact_age, timedelta(days=1))
        if days:
            disconnect_duration = str(days) + " days"
        else:
            hours, remaining = divmod(last_contact_age, timedelta(hours=1))
            if hours:
                disconnect_duration = str(hours) + " hours"
            else:
                minutes, _ = divmod(remaining, timedelta(minutes=1))
                if minutes:
                    disconnect_duration = str(minutes) + " minutes"

        return disconnect_duration

    def post(self):
        """Handle a HTTP POST request."""
        self._authenticate()
        self._validate_request()
        self._pair_device()

        return "", HTTPStatus.NO_CONTENT

    @use_transaction
    def _pair_device(self):
        """Add the paired device to the database."""
        cache_key = DEVICE_PAIRING_CODE_KEY.format(
            pairing_code=self.validated_request["pairing_code"]
        )
        pairing_data = self._get_pairing_data(cache_key)
        device_id = self._add_device()
        pairing_data["uuid"] = device_id
        self.cache.delete(cache_key)
        self._build_pairing_token(pairing_data)

    def _get_pairing_data(self, cache_key) -> dict:
        """Checking if there's one pairing session for the pairing code.

        :return: the pairing code information from the Redis database
        """
        pairing_cache = self.cache.get(cache_key)
        pairing_data = json.loads(pairing_cache)

        return pairing_data

    def _add_device(self) -> str:
        """Creates a device and associate it to a pairing session.

        :return: the database identifier of the new device
        """
        self._ensure_geography_exists()
        device_id = self.device_repository.add(self.account.id, self.validated_request)

        return device_id

    def _build_pairing_token(self, pairing_data: dict):
        """Add a pairing token to the Redis database.

        :param pairing_data: the pairing data retrieved from Redis
        """
        self.cache.set_with_expiration(
            key=DEVICE_PAIRING_TOKEN_KEY.format(pairing_token=pairing_data["token"]),
            value=json.dumps(pairing_data),
            expiration=ONE_DAY,
        )

    def delete(self, device_id: str):
        """Handle an HTTP DELETE request.

        :param device_id: database identifier of a device
        """
        self._authenticate()
        self._delete_device(device_id)

        return "", HTTPStatus.NO_CONTENT

    def _delete_device(self, device_id: str):
        """Delete the specified device from the database.

        There are other tables related to the device table in the database.  This
        method assumes that the child tables contain "delete cascade" clauses.

        :param device_id: database identifier of a device
        """
        self.device_repository.remove(device_id)
        delete_device_login(device_id, self.cache)

    def patch(self, device_id: str):
        """Handle a HTTP PATCH request.

        :param device_id: database identifier of a device
        """
        self._authenticate()
        self._validate_request()
        self._update_device(device_id)
        self.etag_manager.expire_device_etag_by_device_id(device_id)
        self.etag_manager.expire_device_location_etag_by_device_id(device_id)
        self.etag_manager.expire_device_setting_etag_by_device_id(device_id)

        return "", HTTPStatus.NO_CONTENT

    def _validate_request(self):
        """Validate the contents of the HTTP POST request."""
        if self.request.method == "POST":
            device = NewDeviceRequest(self.request.json)
        else:
            device = UpdateDeviceRequest(self.request.json)
        device.validate()
        self.validated_request = device.to_native()
        self.validated_request.update(
            wake_word=self.validated_request["wake_word"].lower()
        )
        if self.validated_request["release_channel"] is not None:
            self.validated_request.update(
                release_channel=self.validated_request["release_channel"].lower()
            )

    def _ensure_geography_exists(self):
        """If the requested geography is not linked to the account, add it.

        :return: database identifier for the geography
        """
        geography = Geography(
            city=self.validated_request.pop("city"),
            country=self.validated_request.pop("country"),
            region=self.validated_request.pop("region"),
            time_zone=self.validated_request.pop("timezone"),
        )
        geography_repository = GeographyRepository(self.db, self.account.id)
        geography_id = geography_repository.get_geography_id(geography)
        if geography_id is None:
            geography_id = geography_repository.add(geography)

        self.validated_request.update(geography_id=geography_id)

    @use_transaction
    def _update_device(self, device_id: str):
        """Update the device attributes on the database based on the request.

        If the device's continuous delivery is managed by Pantacor, attempt the
        Pantacor API calls first.  That way, if they fail, the database updates won't
        happen and we won't get stuck in a half-updated state.

        :param device_id: database identifier of a device
        """
        device = self.device_repository.get_device_by_id(device_id)
        if device.pantacor_config.pantacor_id is not None:
            self._update_pantacor_config(device)
        self._ensure_geography_exists()
        self.device_repository.update_device_from_account(
            self.account.id, device_id, self.validated_request
        )

    def _update_pantacor_config(self, device: Device):
        """Update the Pantacor configuration on the database based on the request.

        :param device: data object representing a Mycroft-enabled device
        """
        new_pantacor_config = dict(
            auto_update=self.validated_request.pop("auto_update"),
            release_channel=self.validated_request.pop("release_channel"),
            ssh_public_key=self.validated_request.pop("ssh_public_key"),
        )
        pantacor_channel_name = self._convert_release_channel(
            new_pantacor_config["release_channel"]
        )
        new_pantacor_config.update(release_channel=pantacor_channel_name)
        old_pantacor_config = asdict(device.pantacor_config)
        update_pantacor_config(old_pantacor_config, new_pantacor_config)
        self.device_repository.update_pantacor_config(device.id, new_pantacor_config)

    def _convert_release_channel(self, release_channel: str) -> str:
        """Converts the channel sent in the request to one recognized by Pantacor.

        :param release_channel: the value of the release channel in the request
        :returns: the release channel as recognized by Pantacor
        """
        pantacor_channel_name = None
        for channel_name, channel_display in self.pantacor_channels.items():
            if channel_display.lower() == release_channel:
                pantacor_channel_name = channel_name

        _log.info("pantacor channel name: %s", pantacor_channel_name)
        return pantacor_channel_name


================================================
FILE: api/account/account_api/endpoints/device_count.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus
from selene.api import SeleneEndpoint
from selene.data.device import DeviceRepository


class DeviceCountEndpoint(SeleneEndpoint):
    def get(self):
        self._authenticate()
        device_count = self._get_devices()

        return dict(deviceCount=device_count), HTTPStatus.OK

    def _get_devices(self):
        device_repository = DeviceRepository(self.db)
        device_count = device_repository.get_account_device_count(self.account.id)

        return device_count


================================================
FILE: api/account/account_api/endpoints/geography.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.device import GeographyRepository


class GeographyEndpoint(SeleneEndpoint):
    def get(self):
        self._authenticate()
        response_data = self._build_response_data()

        return response_data, HTTPStatus.OK

    def _build_response_data(self):
        geography_repository = GeographyRepository(self.db, self.account.id)
        geographies = geography_repository.get_account_geographies()

        response_data = []
        for geography in geographies:
            response_data.append(
                dict(id=geography.id, name=geography.country, user_defined=True)
            )

        return response_data


================================================
FILE: api/account/account_api/endpoints/membership.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.account import MembershipRepository


class MembershipEndpoint(SeleneEndpoint):
    def get(self):
        membership_repository = MembershipRepository(self.db)
        membership_types = membership_repository.get_membership_types()
        for membership_type in membership_types:
            membership_type.rate = float(membership_type.rate)

        return membership_types, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/pairing_code.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint


class PairingCodeEndpoint(SeleneEndpoint):
    def __init__(self):
        super(PairingCodeEndpoint, self).__init__()
        self.cache = self.config["SELENE_CACHE"]

    def get(self, pairing_code):
        self._authenticate()
        pairing_code_is_valid = self._get_pairing_data(pairing_code)

        return dict(isValid=pairing_code_is_valid), HTTPStatus.OK

    def _get_pairing_data(self, pairing_code: str) -> bool:
        """Checking if there's one pairing session for the pairing code."""
        pairing_code_is_valid = False
        cache_key = "pairing.code:" + pairing_code
        pairing_cache = self.cache.get(cache_key)
        if pairing_cache is not None:
            pairing_code_is_valid = True

        return pairing_code_is_valid


================================================
FILE: api/account/account_api/endpoints/preferences.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from dataclasses import asdict
from http import HTTPStatus

from schematics import Model
from schematics.types import StringType

from selene.api import SeleneEndpoint
from selene.api.etag import ETagManager
from selene.data.device import AccountPreferences, PreferenceRepository


class PreferencesRequest(Model):
    date_format = StringType(required=True, choices=["DD/MM/YYYY", "MM/DD/YYYY"])
    measurement_system = StringType(required=True, choices=["Imperial", "Metric"])
    time_format = StringType(required=True, choices=["12 Hour", "24 Hour"])


class PreferencesEndpoint(SeleneEndpoint):
    def __init__(self):
        super(PreferencesEndpoint, self).__init__()
        self.preferences = None
        self.cache = self.config["SELENE_CACHE"]
        self.etag_manager: ETagManager = ETagManager(self.cache, self.config)

    def get(self):
        self._authenticate()
        self._get_preferences()
        if self.preferences is None:
            response_data = ""
            response_code = HTTPStatus.NO_CONTENT
        else:
            response_data = asdict(self.preferences)
            response_code = HTTPStatus.OK

        return response_data, response_code

    def _get_preferences(self):
        preference_repository = PreferenceRepository(self.db, self.account.id)
        self.preferences = preference_repository.get_account_preferences()

    def post(self):
        self._authenticate()
        self._validate_request()
        self._upsert_preferences()
        self.etag_manager.expire_device_setting_etag_by_account_id(self.account.id)
        return "", HTTPStatus.NO_CONTENT

    def patch(self):
        self._authenticate()
        self._validate_request()
        self._upsert_preferences()
        self.etag_manager.expire_device_setting_etag_by_account_id(self.account.id)
        return "", HTTPStatus.NO_CONTENT

    def _validate_request(self):
        self.preferences = PreferencesRequest()
        self.preferences.date_format = self.request.json["dateFormat"]
        self.preferences.measurement_system = self.request.json["measurementSystem"]
        self.preferences.time_format = self.request.json["timeFormat"]
        self.preferences.validate()

    def _upsert_preferences(self):
        preferences_repository = PreferenceRepository(self.db, self.account.id)
        preferences = AccountPreferences(**self.preferences.to_native())
        preferences_repository.upsert(preferences)


================================================
FILE: api/account/account_api/endpoints/region.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.geography import RegionRepository


class RegionEndpoint(SeleneEndpoint):
    def get(self):
        country_id = self.request.args["country"]
        region_repository = RegionRepository(self.db)
        regions = region_repository.get_regions_by_country(country_id)

        return regions, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/skill_oauth.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os

import requests

from selene.api import SeleneEndpoint


class SkillOauthEndpoint(SeleneEndpoint):
    def __init__(self):
        super(SkillOauthEndpoint, self).__init__()
        self.oauth_base_url = os.environ["OAUTH_BASE_URL"]

    def get(self, oauth_id):
        self._authenticate()
        return self._get_oauth_url(oauth_id)

    def _get_oauth_url(self, oauth_id):
        url = "{base_url}/auth/{oauth_id}/auth_url?uuid={account_id}".format(
            base_url=self.oauth_base_url, oauth_id=oauth_id, account_id=self.account.id
        )
        response = requests.get(url)
        return response.text, response.status_code


================================================
FILE: api/account/account_api/endpoints/skill_settings.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""Endpoint to return the skill settings for a given skill family."""
from http import HTTPStatus

from flask import json, Response

from selene.api import SeleneEndpoint
from selene.api.etag import ETagManager
from selene.data.skill import SkillSettingRepository, AccountSkillSetting


class SkillSettingsEndpoint(SeleneEndpoint):
    _setting_repository = None

    def __init__(self):
        super(SkillSettingsEndpoint, self).__init__()
        self.account_skills = None
        self.family_settings = None
        self.etag_manager: ETagManager = ETagManager(
            self.config["SELENE_CACHE"], self.config
        )

    @property
    def setting_repository(self):
        """Only instantiate the SkillSettingsRepository if needed."""
        if self._setting_repository is None:
            self._setting_repository = SkillSettingRepository(self.db)

        return self._setting_repository

    def get(self, skill_family_name):
        """Process an HTTP GET request"""
        self._authenticate()
        self.family_settings = self.setting_repository.get_family_settings(
            self.account.id, skill_family_name
        )
        self._parse_selection_options()
        response_data = self._build_response_data()

        # The response object is manually built here to bypass the
        # camel case conversion so settings are displayed correctly
        return Response(
            response=json.dumps(response_data),
            status=HTTPStatus.OK,
            content_type="application/json",
        )

    def _parse_selection_options(self):
        """Parse the dropdown options string into a list of options.

        Drop-down options are defined in a skill's settingsmeta.json as such:
            <label 1>|<value 1>;<label 2>|<value 2>;<label 3>|<value 3>...etc
        Convert this string into a dictionary where the key is the label and
        the value is the value.
        """
        for skill_settings in self.family_settings:
            if skill_settings.settings_definition is not None:
                for section in skill_settings.settings_definition["sections"]:
                    for field in section["fields"]:
                        if field["type"] == "select":
                            parsed_options = []
                            for option in field["options"].split(";"):
                                option_display, option_value = option.split("|")
                                parsed_options.append(
                                    dict(display=option_display, value=option_value)
                                )
                            field["options"] = parsed_options

    def _build_response_data(self):
        """Build the object to return to the UI."""
        response_data = []
        for skill_settings in self.family_settings:
            # The UI will throw an error if settings display is null due to how
            # the skill settings data structures are defined.
            if skill_settings.settings_definition is None:
                skill_settings.settings_definition = dict(sections=[])
            response_skill = dict(
                settingsDisplay=skill_settings.settings_definition,
                settingsValues=skill_settings.settings_values,
                deviceNames=skill_settings.device_names,
            )
            response_data.append(response_skill)

        return response_data

    def put(self, skill_family_name):
        """Process a HTTP PUT request"""
        self._authenticate()
        self._update_settings_values()

        return "", HTTPStatus.OK

    def _update_settings_values(self):
        """Update the value of the settings column on the device_skill table,"""
        for new_skill_settings in self.request.json["skillSettings"]:
            account_skill_settings = AccountSkillSetting(
                settings_definition=new_skill_settings["settingsDisplay"],
                settings_values=new_skill_settings["settingsValues"],
                device_names=new_skill_settings["deviceNames"],
            )
            self.setting_repository.update_skill_settings(
                self.account.id, account_skill_settings, self.request.json["skillIds"]
            )
        self.etag_manager.expire_skill_etag_by_account_id(self.account.id)


================================================
FILE: api/account/account_api/endpoints/skills.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.skill import SkillRepository


class SkillsEndpoint(SeleneEndpoint):
    def get(self):
        self._authenticate()
        response_data = self._build_response_data()

        return response_data, HTTPStatus.OK

    def _build_response_data(self):
        skill_repository = SkillRepository(self.db)
        skills = skill_repository.get_skills_for_account(self.account.id)

        response_data = {}
        for skill in skills:
            try:
                response_skill = response_data[skill.family_name]
            except KeyError:
                response_data[skill.family_name] = dict(
                    family_name=skill.family_name,
                    market_id=skill.market_id,
                    name=skill.display_name or skill.family_name,
                    has_settings=skill.has_settings,
                    skill_ids=skill.skill_ids,
                )
            else:
                response_skill["skill_ids"].extend(skill.skill_ids)
                if response_skill["market_id"] is None:
                    response_skill["market_id"] = skill.market_id

        return sorted(response_data.values(), key=lambda x: x["name"])


================================================
FILE: api/account/account_api/endpoints/software_update.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2021 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Endpoint to process a user's request to apply an software update on their device."""
from http import HTTPStatus

from schematics import Model
from schematics.types import StringType

from selene.api import SeleneEndpoint
from selene.api.pantacor import apply_pantacor_update


class SoftwareUpdateRequest(Model):
    """Schematic for a request to update software on a device."""

    deployment_id = StringType(required=True)


class SoftwareUpdateEndpoint(SeleneEndpoint):
    """Send a request to Pantacor to update a device."""

    def patch(self):
        """Handle a HTTP PATCH request."""
        self._authenticate()
        self._validate_request()
        apply_pantacor_update(self.request.json["deploymentId"])
        return "", HTTPStatus.NO_CONTENT

    def _validate_request(self):
        """Validate the contents of the PATCH request."""
        request_validator = SoftwareUpdateRequest()
        request_validator.deployment_id = self.request.json["deploymentId"]
        request_validator.validate()


================================================
FILE: api/account/account_api/endpoints/ssh_key_validator.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2021 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Endpoint to validate the contents of the SSH public key."""
from urllib.parse import unquote_plus
from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.util.ssh import validate_rsa_public_key


class SshKeyValidatorEndpoint(SeleneEndpoint):
    """Validate the contents of an SSH public key."""

    def get(self):
        """Handle and HTTP GET request.

        The SSH key is encoded in the UI because it can contain characters that are
        reserved for URL delimiting.
        """
        self._authenticate()
        decoded_ssh_key = unquote_plus(self.request.args["key"])
        ssh_key_is_valid = validate_rsa_public_key(decoded_ssh_key)

        return dict(isValid=ssh_key_is_valid), HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/timezone.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.geography import TimezoneRepository


class TimezoneEndpoint(SeleneEndpoint):
    def get(self):
        country_id = self.request.args["country"]
        timezone_repository = TimezoneRepository(self.db)
        timezones = timezone_repository.get_timezones_by_country(country_id)

        for timezone in timezones:
            timezone.dst_offset = float(timezone.dst_offset)
            timezone.gmt_offset = float(timezone.gmt_offset)

        return timezones, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/verify_email_address.py
================================================
#  Mycroft Server - Backend
#  Copyright (c) 2022 Mycroft AI Inc
#  SPDX-License-Identifier: 	AGPL-3.0-or-later
#  #
#  This file is part of the Mycroft Server.
#  #
#  The Mycroft Server is free software: you can redistribute it and/or
#  modify it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#  #
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU Affero General Public License for more details.
#  #
#  You should have received a copy of the GNU Affero General Public License
#  along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Account API endpoint to be called when a user is verifying their email address."""

from binascii import a2b_base64
from http import HTTPStatus

from selene.api import SeleneEndpoint, APIError
from selene.data.account import AccountRepository
from selene.util.email import validate_email_address


class VerifyEmailAddressEndpoint(SeleneEndpoint):
    """Updates a user's email address after they have verified it."""

    def put(self):
        """Processes an HTTP PUT request to update the email address."""
        self._authenticate()
        email_address = self._validate_email_address()
        self._update_account(email_address)

        return "", HTTPStatus.NO_CONTENT

    def _validate_email_address(self) -> str:
        """Validates that the email address is well formatted and reachable.

        By this point in the email address change process, this validation has
        already been done.  It is done again here as a protection against malicious
        calls to this endpoint.

        :returns: a normalized version of the email address in the request
        :raises: an API error if the email address validation fails
        """
        encoded_email_address = self.request.json["token"]
        email_address = a2b_base64(encoded_email_address).decode()
        normalized_email_address, error = validate_email_address(email_address)
        if error is not None:
            raise APIError(f"invalid email address: {error}")

        return normalized_email_address

    def _update_account(self, email_address: str):
        """Updates the email address on the DB now that it has been verified.

        :param email_address: the email address to apply to the account.account table
        """
        account_repo = AccountRepository(self.db)
        account_repo.update_email_address(self.account.id, email_address)


================================================
FILE: api/account/account_api/endpoints/voice_endpoint.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.device import TextToSpeechRepository


class VoiceEndpoint(SeleneEndpoint):
    def get(self):
        tts_repository = TextToSpeechRepository(self.db)
        voices = tts_repository.get_voices()

        response_data = []
        for voice in voices:
            response_data.append(
                dict(id=voice.id, name=voice.display_name, user_defined=False)
            )

        return response_data, HTTPStatus.OK


================================================
FILE: api/account/account_api/endpoints/wake_word_endpoint.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/
"""Account API endpoint to return a list of available wake words."""

from http import HTTPStatus

from selene.api import SeleneEndpoint
from selene.data.wake_word import WakeWordRepository


class WakeWordEndpoint(SeleneEndpoint):
    """Return a list of available wake words"""

    def get(self):
        """Handle a HTTP GET request."""
        self._authenticate()
        response_data = self._build_response_data()

        return response_data, HTTPStatus.OK

    def _build_response_data(self):
        """Build the response to the HTTP GET request."""
        response_data = []
        wake_word_repository = WakeWordRepository(self.db)
        wake_words = wake_word_repository.get_wake_words_for_web()
        for wake_word in wake_words:
            response_data.append(
                dict(id=wake_word.id, name=wake_word.name, user_defined=False,)
            )

        return response_data


================================================
FILE: api/account/pyproject.toml
================================================
[tool.poetry]
name = "account"
version = "0.1.0"
description = "API to support account.mycroft.ai"
authors = ["Chris Veilleux <veilleux.chris@gmail.com>"]
license = "GNU AGPL 3.0"

[tool.poetry.dependencies]
python = "^3.9"
# Version 1.0 of flask required because later versions do not allow lists to be passed as API repsonses.  The Google
# STT endpoint passes a list of transcriptions to the device.  Changing this to return a dictionary would break the
# API's V1 contract with Mycroft Core.
#
# To make flask 1.0 work, older versions of itsdangerous, jinja2, markupsafe and werkszeug are required.
flask = "<1.1"
itsdangerous = "<=2.0.1"
jinja2 = "<=2.10.1"
markupsafe = "<=2.0.1"
schematics = "*"
stripe = "*"
selene = {path = "./../../shared",  develop = true}
uwsgi = "*"
werkzeug = "<=2.0.3"

[tool.poetry.dev-dependencies]
allure-behave = "*"
behave = "*"
pyhamcrest = "*"
pylint = "*"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"


================================================
FILE: api/account/tests/features/add_device.feature
================================================
Feature: Account API -- Pair a device
  Test the device add endpoint

  Scenario: Add a device
    Given an account
    And the account is authenticated
    And a device pairing code
    When an API request is sent to add a device
    Then the request will be successful
    And the device is added to the database
    And the pairing code is removed from cache
    And the pairing token is added to cache


================================================
FILE: api/account/tests/features/agreements.feature
================================================
Feature: Account API -- Get the active agreements
  We need to be able to retrieve an agreement and display it on the web app.

  Scenario: Multiple versions of an agreement exist
     When API request for Privacy Policy is made
     Then the request will be successful
     And Privacy Policy version 999 is returned


  Scenario: Retrieve Terms of Use
     When API request for Terms of Use is made
     Then the request will be successful
     And Terms of Use version 999 is returned


================================================
FILE: api/account/tests/features/authentication.feature
================================================
Feature: Account API - Authentication with JWTs
  Some of the API endpoints contain information that is specific to a user.
  To ensure that information is seen only by the user that owns it, we will
  use a login mechanism coupled with authentication tokens to securely identify
  a user.

  The code executed in these tests is embedded in every view call. These tests
  apply to any endpoint that requires authentication.  These tests are meant to
  be the only place authentication logic needs to be tested.

  Scenario: Request for user data includes valid access token
    Given an account with a valid access token
    When a user requests their profile
    Then the request will be successful
    And the authentication tokens will remain unchanged

  Scenario: Access token expired
    Given an account with an expired access token
    When a user requests their profile
    Then the request will be successful
    And the authentication tokens will be refreshed

  Scenario: Access token missing but refresh token valid
    Given an account with a refresh token but no access token
    When a user requests their profile
    Then the request will be successful
    And the authentication tokens will be refreshed

  Scenario: Both access and refresh tokens expired
    Given an account with expired access and refresh tokens
    When a user requests their profile
    Then the request will fail with an unauthorized error


================================================
FILE: api/account/tests/features/environment.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Setup the environment for the account API behavioral tests."""
from datetime import datetime

from behave import fixture, use_fixture

from account_api.api import acct
from selene.data.metric import AccountActivityRepository
from selene.testing.account import add_account, remove_account
from selene.testing.account_geography import add_account_geography
from selene.testing.agreement import add_agreements, remove_agreements
from selene.testing.tagging import remove_wake_word_files
from selene.testing.text_to_speech import add_text_to_speech, remove_text_to_speech
from selene.testing.wake_word import add_wake_word, remove_wake_word
from selene.util.cache import SeleneCache
from selene.util.db import connect_to_db


@fixture
def acct_api_client(context):
    """Add a test fixture representing the account API."""
    acct.testing = True
    context.client_config = acct.config
    context.client = acct.test_client()

    yield context.client


def before_all(context):
    """Setup static test data before any tests run.

    This is data that does not change from test to test so it only needs to be setup
    and torn down once.
    """
    use_fixture(acct_api_client, context)
    context.db = connect_to_db(context.client_config["DB_CONNECTION_CONFIG"])
    add_agreements(context)
    context.wake_word = add_wake_word(context.db)


def after_all(context):
    """Clean up static test data after all tests have run.

    This is data that does not change from test to test so it only needs to be setup
    and torn down once.
    """
    remove_wake_word(context.db, context.wake_word)
    remove_agreements(
        context.db, [context.privacy_policy, context.terms_of_use, context.open_dataset]
    )


def before_scenario(context, _):
    """Setup data that could change during a scenario so each test starts clean."""
    account = add_account(context.db)
    context.accounts = dict(foo=account)
    context.geography_id = add_account_geography(context.db, account)
    context.voice = add_text_to_speech(context.db)
    acct_activity_repository = AccountActivityRepository(context.db)
    context.account_activity = acct_activity_repository.get_activity_by_date(
        datetime.utcnow().date()
    )


def after_scenario(context, _):
    """Cleanup data that could change during a scenario so next scenario starts fresh.

    The database is setup with cascading deletes that take care of cleaning up[
    referential integrity for us.  All we have to do here is delete the account
    and all rows on all tables related to that account will also be deleted.
    """
    for account in context.accounts.values():
        remove_account(context.db, account)
    remove_text_to_speech(context.db, context.voice)
    _clean_cache()
    if hasattr(context, "wake_word_file"):
        remove_wake_word_files(context.db, context.wake_word_file)


def _clean_cache():
    """Remove testing data from the Redis database."""
    cache = SeleneCache()
    cache.delete("pairing.token:this is a token")


================================================
FILE: api/account/tests/features/pantacor_update.feature
================================================
Feature: Account API -- Interact with the Pantacor API
  Devices that use Pantacor to manage the software running on them have a set of
  additional attributes that can be updated using the Pantacor API

  Scenario: Indicate to user that software update is available
    Given an account
    And the account is authenticated
    And a device using Pantacor for continuous delivery
    And the device has pending deployment from Pantacor
    When the user requests to view the device
    Then the request will be successful
    And the response contains the pending deployment ID

  Scenario: User elects to apply a software update
    Given an account
    And the account is authenticated
    And a device using Pantacor for continuous delivery
    And the device has pending deployment from Pantacor
    When the user selects to apply the update
    Then the request will be successful

  Scenario: User enters a valid SSH key
    Given an account
    And the account is authenticated
    And a device using Pantacor for continuous delivery
    When the user enters a well formed RSA SSH key
    Then the request will be successful
    And the response indicates that the SSH key is properly formatted

  Scenario: User enters an invalid SSH key
    Given an account
    And the account is authenticated
    And a device using Pantacor for continuous delivery
    When the user enters a malformed RSA SSH key
    Then the request will be successful
    And the response indicates that the SSH key is malformed


================================================
FILE: api/account/tests/features/profile.feature
================================================
Feature: Account API -- Manage account profiles
  Test the ability of the account API to retrieve and manage a user's profile
  settings.

  Scenario: Retrieve authenticated user's account
    Given an account with a monthly membership
    When a user requests their profile
    Then the request will be successful
    And user profile is returned

  Scenario: user with free account opts into a membership
    Given an account without a membership
    And the account is authenticated
    When a monthly membership is added
    Then the request will be successful
    And the account should have a monthly membership
    And the new member will be reflected in the account activity metrics

  Scenario: user opts out monthly membership
    Given an account with a monthly membership
    When the membership is cancelled
    Then the request will be successful
    And the account should have no membership
    And the deleted member will be reflected in the account activity metrics

  Scenario: user changes from a monthly membership to yearly membership
    Given an account with a monthly membership
    When the membership is changed to yearly
    Then the request will be successful
    And the account should have a yearly membership

  Scenario: user opts into the open dataset
    Given an account opted out of the Open Dataset agreement
    And the account is authenticated
    When the user opts into the open dataset
    Then the request will be successful
    And the account will have a open dataset agreement
    And the new agreement will be reflected in the account activity metrics

  Scenario: user opts out of the open dataset
    Given an account opted into the Open Dataset agreement
    And the account is authenticated
    When the user opts out of the open dataset
    Then the request will be successful
    And the account will not have a open dataset agreement
    And the deleted agreement will be reflected in the account activity metrics

  Scenario: User changes password
    Given a user who authenticates with a password
    And the account is authenticated
    When the user changes their password
    Then the request will be successful
    And the password on the account will be changed
    And an password change notification will be sent

  Scenario: User changes email address
    Given a user who authenticates with a password
    And the account is authenticated
    When the user changes their email address
    Then the request will be successful
    And an email change notification will be sent to the old email address
    And an email change verification message will be sent to the new email address

  Scenario: User changes email address to a value is assigned to an existing account
    Given a user who authenticates with a password
    And the account is authenticated
    When the user changes their email address to that of an existing account
    Then the request will be successful
    And a duplicate email address error is returned


================================================
FILE: api/account/tests/features/remove_account.feature
================================================
Feature: Account API -- Delete an account
  Test the API call to delete an account and all its related data from the database.

  Scenario: Successful account deletion
    Given an account
    And the account is authenticated
    When a user requests to delete their account
    Then the request will be successful
    And the user's account is deleted
    And the deleted account will be reflected in the account activity metrics

  Scenario: Membership removed upon account deletion
    Given an account with a monthly membership
    When a user requests to delete their account
    Then the request will be successful
    And the membership is removed from stripe

  Scenario: Wake word files removed upon account deletion
    Given an account opted into the Open Dataset agreement
    And a wake word sample contributed by the user
    And the account is authenticated
    When a user requests to delete their account
    Then the request will be successful
    And the wake word contributions are flagged for deletion


================================================
FILE: api/account/tests/features/steps/add_device.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Python code to support the add device feature."""
import json

from behave import given, when, then  # pylint: disable=no-name-in-module
from hamcrest import assert_that, equal_to, none, not_none

from selene.data.device import DeviceRepository
from selene.util.cache import (
    DEVICE_PAIRING_CODE_KEY,
    DEVICE_PAIRING_TOKEN_KEY,
    SeleneCache,
)
from selene.util.db import connect_to_db


@given("a device pairing code")
def set_device_pairing_code(context):
    """Add dummy data to the Redis cache for the test."""
    pairing_data = dict(
        code="ABC123",
        packaging_type="pantacor",
        state="this is a state",
        token="this is a token",
        expiration=84600,
    )
    cache = SeleneCache()
    cache.set_with_expiration(
        "pairing.code:ABC123", json.dumps(pairing_data), expiration=86400
    )
    context.pairing_data = pairing_data
    context.pairing_code = "ABC123"


@when("an API request is sent to add a device")
def add_device(context):
    """Call the endpoint to add a device based on user input."""
    device = dict(
        city="Kansas City",
        country="United States",
        name="Selene Test Device",
        pairingCode=context.pairing_code,
        placement="Mycroft Offices",
        region="Missouri",
        timezone="America/Chicago",
        wakeWord="hey selene",
        voice="Selene Test Voice",
    )
    response = context.client.post(
        "/api/devices", data=json.dumps(device), content_type="application/json"
    )
    context.response = response


@then("the pairing code is removed from cache")
def validate_pairing_code_removal(context):
    """Ensure that the endpoint removed the pairing code entry from the cache."""
    cache = SeleneCache()
    pairing_data = cache.get(
        DEVICE_PAIRING_CODE_KEY.format(pairing_code=context.pairing_code)
    )
    assert_that(pairing_data, none())


@then("the device is added to the database")
def validate_response(context):
    """Ensure that the database was updated as expected."""
    account = context.accounts["foo"]
    db = connect_to_db(context.client_config["DB_CONNECTION_CONFIG"])
    device_repository = DeviceRepository(db)
    devices = device_repository.get_devices_by_account_id(account.id)
    device = None
    for device in devices:
        if device.name == "Selene Test Device":
            break
    assert_that(device, not_none())
    assert_that(device.name, equal_to("Selene Test Device"))
    assert_that(device.placement, equal_to("Mycroft Offices"))
    assert_that(device.account_id, equal_to(account.id))
    context.device_id = device.id


@then("the pairing token is added to cache")
def validate_pairing_token(context):
    """Validate the pairing token data was added to the cache as expected."""
    cache = SeleneCache()
    pairing_data = cache.get(
        DEVICE_PAIRING_TOKEN_KEY.format(pairing_token="this is a token")
    )
    pairing_data = json.loads(pairing_data)

    assert_that(pairing_data["uuid"], equal_to(context.device_id))
    assert_that(pairing_data["state"], equal_to(context.pairing_data["state"]))
    assert_that(pairing_data["token"], equal_to(context.pairing_data["token"]))


================================================
FILE: api/account/tests/features/steps/agreements.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from dataclasses import asdict
import json

from behave import then, when
from hamcrest import assert_that, equal_to

from selene.data.account import PRIVACY_POLICY, TERMS_OF_USE


@when("API request for {agreement} is made")
def call_agreement_endpoint(context, agreement):
    if agreement == PRIVACY_POLICY:
        url = "/api/agreement/privacy-policy"
    elif agreement == TERMS_OF_USE:
        url = "/api/agreement/terms-of-use"
    else:
        raise ValueError("invalid agreement type")

    context.response = context.client.get(url)


@then("{agreement} version {version} is returned")
def validate_response(context, agreement, version):
    response_data = json.loads(context.response.data)
    if agreement == PRIVACY_POLICY:
        expected_response = asdict(context.privacy_policy)
    elif agreement == TERMS_OF_USE:
        expected_response = asdict(context.terms_of_use)
    else:
        raise ValueError("invalid agreement type")

    del expected_response["effective_date"]
    assert_that(response_data, equal_to(expected_response))


================================================
FILE: api/account/tests/features/steps/authentication.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from behave import given, then
from hamcrest import assert_that, equal_to, is_not

from selene.testing.api import (
    generate_access_token,
    generate_refresh_token,
    set_access_token_cookie,
    set_refresh_token_cookie,
    validate_token_cookies,
)
from selene.util.auth import AuthenticationToken

EXPIRE_IMMEDIATELY = 0


@given("an account with a valid access token")
def use_account_with_valid_access_token(context):
    context.username = "foo"
    context.access_token = generate_access_token(context)
    set_access_token_cookie(context)
    context.refresh_token = generate_refresh_token(context)
    set_refresh_token_cookie(context)


@given("an account with an expired access token")
def generate_expired_access_token(context):
    context.username = "foo"
    context.access_token = generate_access_token(context, duration=EXPIRE_IMMEDIATELY)
    set_access_token_cookie(context, duration=EXPIRE_IMMEDIATELY)
    context.refresh_token = generate_refresh_token(context)
    set_refresh_token_cookie(context)
    context.old_refresh_token = context.refresh_token.jwt


@given("an account with a refresh token but no access token")
def generate_refresh_token_only(context):
    context.username = "foo"
    context.refresh_token = generate_refresh_token(context)
    set_refresh_token_cookie(context)
    context.old_refresh_token = context.refresh_token.jwt


@given("an account with expired access and refresh tokens")
def expire_both_tokens(context):
    context.username = "foo"
    context.access_token = generate_access_token(context, duration=EXPIRE_IMMEDIATELY)
    set_access_token_cookie(context, duration=EXPIRE_IMMEDIATELY)
    context.refresh_token = generate_refresh_token(context, duration=EXPIRE_IMMEDIATELY)
    set_refresh_token_cookie(context, duration=EXPIRE_IMMEDIATELY)


@then("the authentication tokens will remain unchanged")
def check_for_no_new_cookie(context):
    cookies = context.response.headers.getlist("Set-Cookie")
    assert_that(cookies, equal_to([]))


@then("the authentication tokens will be refreshed")
def check_for_new_cookies(context):
    validate_token_cookies(context)
    assert_that(context.refresh_token, is_not(equal_to(context.old_refresh_token)))
    refresh_token = AuthenticationToken(context.client_config["REFRESH_SECRET"], 0)
    refresh_token.jwt = context.refresh_token
    refresh_token.validate()
    assert_that(refresh_token.is_valid, equal_to(True), "refresh token valid")
    assert_that(refresh_token.is_expired, equal_to(False), "refresh token expired")
    assert_that(refresh_token.account_id, equal_to(context.accounts["foo"].id))


================================================
FILE: api/account/tests/features/steps/common.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from http import HTTPStatus

from behave import given, then
from hamcrest import assert_that, equal_to, is_in

from selene.testing.api import (
    generate_access_token,
    generate_refresh_token,
    set_access_token_cookie,
    set_refresh_token_cookie,
)


@given("an account")
def define_account(context):
    context.username = "foo"


@given("the account is authenticated")
def use_account_with_valid_access_token(context):
    context.access_token = generate_access_token(context)
    set_access_token_cookie(context)
    context.refresh_token = generate_refresh_token(context)
    set_refresh_token_cookie(context)


@then("the request will be successful")
def check_request_success(context):
    assert_that(
        context.response.status_code, is_in([HTTPStatus.OK, HTTPStatus.NO_CONTENT])
    )


@then("the request will fail with {error_type} error")
def check_for_bad_request(context, error_type):
    if error_type == "a bad request":
        assert_that(context.response.status_code, equal_to(HTTPStatus.BAD_REQUEST))
    elif error_type == "an unauthorized":
        assert_that(context.response.status_code, equal_to(HTTPStatus.UNAUTHORIZED))
    else:
        raise ValueError("unsupported error_type")


================================================
FILE: api/account/tests/features/steps/pantacor_update.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Step functions for applying a software update via the account API."""

import json
from unittest.mock import MagicMock, patch

from behave import given, then, when  # pylint: disable=no-name-in-module
from hamcrest import assert_that, equal_to

from selene.testing.device import add_device, add_pantacor_config


@given("a device using Pantacor for continuous delivery")
def add_pantacor_device(context):
    """Add a device with a Pantacor config and software update set to manual."""
    context.device_id = add_device(
        context.db, context.accounts["foo"].id, context.geography_id
    )
    add_pantacor_config(context.db, context.device_id)


@given("the device has pending deployment from Pantacor")
def add_pantacor_deployment_id(context):
    """Add a dummy deployment ID to the context for use later in tests."""
    context.deployment_id = "test_deployment_id"


@when("the user selects to apply the update")
def apply_software_update(context):
    """Make an API call to apply the software update.

    The Pantacor API code is patched because there is currently no way to call it
    reliably with a test device.
    """
    with patch("requests.request") as request_patch:
        apply_update_response = MagicMock(spec=["ok", "content"])
        apply_update_response.ok = True
        apply_update_response.content = '{"response":"ok"}'.encode()
        request_patch.side_effect = [apply_update_response]
        request_data = dict(deploymentId=context.deployment_id)
        response = context.client.patch(
            "/api/software-update",
            data=json.dumps(request_data),
            content_type="application/json",
        )
    context.response = response


@when("the user enters a malformed RSA SSH key")
def validate_invalid_ssh_key(context):
    """Make an API call to check the validity of a RSA SSH key."""
    response = context.client.get(
        "/api/ssh-key?key=foo", content_type="application/json"
    )
    context.response = response


@when("the user enters a well formed RSA SSH key")
def validate_valid_ssh_key(context):
    """Make an API call to check the validity of a RSA SSH key."""
    response = context.client.get(
        "/api/ssh-key?key=ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAACAQDEwmtmRho==%20foo",
        content_type="application/json",
    )
    context.response = response


@when("the user requests to view the device")
def get_device(context):
    """Make an API call to get device data, including a software update ID.

    The Pantacor API code is patched because there is currently no way to call it
    reliably with a test device.
    """
    with patch("requests.request") as request_patch:
        api_response = MagicMock(spec=["ok", "content"])
        api_response.ok = True
        deployment = dict(id="test_deployment_id")
        get_deployment_content = dict(items=[deployment])
        api_response.content = json.dumps(get_deployment_content).encode()
        request_patch.side_effect = [api_response]
        response = context.client.get(
            "/api/devices/" + context.device_id, content_type="application/json"
        )
    context.response = response


@then("the response contains the pending deployment ID")
def check_for_deployment_id(context):
    """Check the response of the device query to ensure the update ID is populated."""
    device_attributes = context.response.json
    assert_that(
        device_attributes["pantacorConfig"]["deploymentId"],
        equal_to("test_deployment_id"),
    )


@then("the response indicates that the SSH key is malformed")
def check_for_malformed_ssh_key(context):
    """Ensure the response indicates the SSH key passed on the URL is invalid"""
    response = context.response
    assert_that(response.json, equal_to(dict(isValid=False)))


@then("the response indicates that the SSH key is properly formatted")
def check_for_well_formed_ssh_key(context):
    """Ensure the response indicates the SSH key passed on the URL is valid"""
    response = context.response
    assert_that(response.json, equal_to(dict(isValid=True)))


================================================
FILE: api/account/tests/features/steps/profile.py
================================================
# Mycroft Server - Backend
# Copyright (C) 2019 Mycroft AI Inc
# SPDX-License-Identifier: 	AGPL-3.0-or-later
#
# This file is part of the Mycroft Server.
#
# The Mycroft Server is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Step functions for maintaining an account profile via the account API."""

import json
from binascii import b2a_base64
from datetime import datetime
from os import environ
from unittest.mock import patch

from behave import given, then, when  # pylint: disable=no-name-in-module
from hamcrest import (
    assert_that,
    equal_to,
    greater_than,
    has_item,
    is_in,
    none,
    not_none,
    starts_with,
)

from selene.data.account import (
    AccountRepository,
    PRIVACY_POLICY,
    TERMS_OF_USE,
    OPEN_DATASET,
)
from selene.data.metric import AccountActivityRepository
from selene.testing.account_activity import check_account_metrics
from selene.testing.api import (
    generate_access_token,
    generate_refresh_token,
    set_access_token_cookie,
    set_refresh_token_cookie,
)
from selene.testing.membership import MONTHLY_MEMBERSHIP, YEARLY_MEMBERSHIP
from selene.util.email import EmailMessage

BAR_EMAIL_ADDRESS = "bar@mycroft.ai"
STRIPE_METHOD = "Stripe"
VISA_TOKEN = "tok_visa"


@given("an account with a monthly membership")
def add_membership_to_account(context):
    """Use the API to add a monthly membership on Stripe

    The API is used so that the Stripe API can be interacted with.
    """
    context.username = "foo"
    context.access_token = generate_access_token(context)
    set_access_token_cookie(context)
    context.refresh_token = generate_refresh_token(context)
    set_refresh_token_cookie(context)
    _add_membership_via_api(context)
    acct_repository = AccountRepository(context.db)
    membership = acct_repository.get_active_account_membership(
        context.accounts["foo"].id
    )
    context.accounts["foo"].membership = membership


@given("an account without a membership")
def get_account_no_membership(context):
    """Set context username to one with no membership."""
    context.username = "foo"


@given("an account opted {in_or_out} the Open Dataset agreement")
def set_account_open_dataset(context, in_or_out):
    """Expire open dataset agreement (assumes default is active agreement)."""
    context.username = "foo"
    if in_or_out == "out of":
        account = context.accounts["foo"]
        account_repo = AccountRepository(context.db)
        account_repo.expire_open_dataset_agreement(account.id)


@given("a user who authenticates with a password")
def setup_user(context):
    """Set user context for use in other steps."""
    context.username = "foo"
    context.password = "barfoo"


@when("the user changes their password")
def call_password_change_endpoint(context):
    """Call the password change endpoint for the single sign on API."""
    change_password_request = dict(
        password=b2a_base64(context.password.encode()).decode()
    )
    with patch("account_api.endpoints.change_password.SeleneMailer") as email_mock:
        response = context.client.put(
            "/api/change-password",
            data=json.dumps(change_password_request),
            content_type="application/json",
        )
        context.response = response
        context.email_mock = email_mock


@when("the user changes their email address")
def call_email_address_change_endpoint(context):
    """Call the password change endpoint for the single sign on API."""
    context.new_email_address = "bar@mycroft.ai"
    encoded_email_address = context.new_email_address.encode()
    context.email_verification_token = b2a_base64(
        encoded_email_address, newline=False
    ).decode()
    change_email_request = dict(token=context.email_verification_token)
    with patch("account_api.endpoints.change_email_address.SeleneMailer") as email_mock:
        response = context.client.put(
            "/api/change-email",
            data=json.dumps(change_email_request),
            content_type="application/json",
        )
        context.response = response
        context.email_mock = email_mock


@when("the user changes their email address to that of an existing account")
def call_email_validation_endpoint(context):
    """Call the email validation endpoint on the account API."""
    existing_account = context.accounts["foo"]
    email_address = existing_account.email_address.encode()
    token = b2a_base64(email_address).decode()

    context.client.content_type = "application/json"
    response = context.client.get(
        f"/api/validate-email?platform=Internal&token={token}"
    )
    context.response = response


@when("a user requests their profile")
def call_account_endpoint(context):
    """Issue API call to retrieve account profile."""
    context.response = context.client.get(
        "/api/account", content_type="application/json"
    )


@when("a monthly membership is added")
def add_monthly_membership(context):
    """Issue API call to add a monthly membership to an account."""
    context.response = _add_membership_via_api(context)


@when("the membership is cancelled")
def cancel_membership(context):
    """Issue API call to cancel and account's membership."""
    membership_data = dict(action="cancel")
    context.response = context.client.patch(
        "/api/account",
        data=json.dumps(dict(membership=membership_data)),
        content_type="application/json",
    )


def _add_membership_via_api(context):
    """Helper function to add account membership via API call"""
    membership_data = dict(
        action="add",
        membershipType=MONTHLY_MEMBERSHIP,
        paymentMethod=STRIPE_METHOD,
        paymentToken=VISA_TOKEN,
    )
    return context.client.patch(
        "/api/account",
        data=json.dumps(dict(membership=membership_data)),
        content_type="application/json",
    )


@when("the membership is changed to yearly")
def change_to_yearly_account(context):
    """Issue API call to change a monthly membership to a yearly membership."""
    membership_data = dict(action="update", membershipType=YEARLY_MEMBERSHIP)
    context.response = context.client.patch(
        "/api/account",
        data=json.dumps(dict(membership=membership_data)),
        content_type="application/json",
    )


@when("the user opts {in_or_out} the open dataset")
def set_open_dataset_status(context, in_or_out):
    """Issue API call to opt into or out of the open dataset agreement."""
    if in_or_out not in ("into", "out of"):
        raise ValueError('User can only opt "into" or "out of" the agreement')
    context.response = context.client.patch(
        "/api/account",
        data=json.dumps(dict(openDataset=in_or_out == "into")),
        content_type="application/json",
    )


@then("user profile is returned")
def validate_response(context):
    """Check results of API call."""
    response_data = context.response.json
    utc_date = datetime.utcnow().date()
    account = context.accounts["foo"]
    assert_that(response_data["emailAddress"], equal_to(account.email_address))
    assert_that(response_data["membership"]["type"], equal_to("Monthly Membership"))
    assert_that(response_data["membership"]["duration"], none())
    assert_that(response_data["membership"], has_item("id"))

    assert_that(len(response_data["agreements"]), equal_to(3))
    for agreement in response_data["agreements"]:
        assert_that(
            agreement["type"], is_in([PRIVACY_POLICY, TERMS_OF_USE, OPEN_DATASET])
        )
        assert_that(
            agreement["acceptDate"], equal_to(str(utc_date.strftime("%B %d, %Y")))
        )
        assert_that(agreement, has_item("id"))


@then("the account should have a monthly membership")
def validate_monthly_account(context):
    """Check that the monthly membership information for an account is accurate."""
    acct_repository = AccountRepository(context.db)
    membership = acct_repository.get_active_account_membership(
        context.accounts["foo"].id
    )
    assert_that(membership.type, equal_to(MONTHLY_MEMBERSHIP))
    assert_that(membership.payment_account_id, starts_with("cus"))
    assert_that(membership.start_date, equal_to(datetime.utcnow().date()))
    assert_that(membership.end_date, none())


@then("the account should have no membership")
def validate_absence_of_membership(context):
    """Check for the absence of a membership on an account."""
    acct_repository = AccountRepository(context.db)
    membership = acct_repository.get_active_account_membership(
        context.accounts["foo"].id
    )
    assert_that(membership, none())


@then("the account should have a yearly membership")
def yearly_account(context):
    """Check that the yearly membership information for an account is accurate."""
    acct_repository = AccountRepository(context.db)
    membership = acct_repository.get_active_account_membership(
        context.accounts["foo"].id
    )
    assert_that(membership.type, equal_to(YEARLY_MEMBERSHIP))
    assert_that(membership.payment_account_id, starts_with("cus"))


@then("the new member will be reflected in the account activity metrics")
def check_new_member_account_metrics(context):
    """Ensure a new membership is accurately reflected in the metrics."""
    check_account_metrics(context, "members", "members_added")


@then("the deleted member will be reflected in the account activity metrics")
def check_expired_member_account_metrics(context):
    """Ensure that the account deletion is recorded in the metrics schema."""
    acct_activity_repository = AccountActivityRepository(context.db)
    account_activity = acct_activity_repository.get_activity_by_date(
        datetime.utcnow().date()
    )
    if context.account_activity is None:
        assert_that(account_activity.members, greater_than(0))
        assert_that(account_activity.members_expired, equal_to(1))
    else:
        # Membership was added in a previous step so rather than the membership being
        # decreased by one, it would net to being the same after the expiration.
        assert_that(
            account_activity.members,
            equal_to(context.account_activity.members),
        )
        assert_that(
            account_activity.members_expired,
            equal_to(context.account_activity.members_expired + 1),
        )


@then("the account {will_or_wont} have a open dataset agreement")
def check_for_open_dataset_agreement(context, will_or_wont):
    """Check the status of the open dataset agreement for an account."""
    account_repo = AccountRepository(context.db)
    account = account_repo.get_account_by_id(context.accounts["foo"].id)
    agreements = [agreement.type for agreement in account.agreements]
    if will_or_wont == "will":
        assert_that(OPEN_DATASET, is_in(agreements))
    elif will_or_wont == "will not":
        assert_that(OPEN_DATASET, not is_in(agreements))
    else:
        raise ValueError('Valid values are only "will" or "won\'t"')


@then("the new agreement will be reflected in the account activity metrics")
def check_new_open_dataset_account_metrics(context):
    """Ensure a new agreement is accurately reflected in the metrics."""
    check_account_metrics(context, "open_dataset", "open_dataset_added")


@then("the deleted agreement will be reflected in the account activity metrics")
def check_deleted_open_dataset_account_metrics(context):
    """Ensure a new agreement is accurately reflected in the metrics."""
    check_account_metrics(context, "open_dataset", "open_dataset_deleted")


@then("the password on the account will be changed")
def check_new_password(context):
    """Retrieves the account with the new password to verify it was changed."""
    acct_repository = AccountRepository(context.db)
    test_account = context.accounts["foo"]
    account = acct_repository.get_account_from_credentials(
        test_account.email_address, context.password
    )
    assert_that(account, not_none())


@then("a duplicate email address error is returned")
def check_for_duplicate_account_error(context):
    """Check the API response for an "account exists" error."""
    response = context.response
    assert_that(response.json["accountExists"], equal_to(True))


@then("an password change notification will be sent")
def check_password_change_notification_sent(context):
    """Ensures the email change notification message was sent.

    Using a mock for email as we don't want to be sending emails every time the tests
    run.
    """
    email_mock = context.email_mock
Download .txt
gitextract_zfonubvk/

├── .editorconfig
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── SUPPORT.md
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── README.md
├── api/
│   ├── account/
│   │   ├── account_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── change_email_address.py
│   │   │       ├── change_password.py
│   │   │       ├── city.py
│   │   │       ├── country.py
│   │   │       ├── defaults.py
│   │   │       ├── device.py
│   │   │       ├── device_count.py
│   │   │       ├── geography.py
│   │   │       ├── membership.py
│   │   │       ├── pairing_code.py
│   │   │       ├── preferences.py
│   │   │       ├── region.py
│   │   │       ├── skill_oauth.py
│   │   │       ├── skill_settings.py
│   │   │       ├── skills.py
│   │   │       ├── software_update.py
│   │   │       ├── ssh_key_validator.py
│   │   │       ├── timezone.py
│   │   │       ├── verify_email_address.py
│   │   │       ├── voice_endpoint.py
│   │   │       └── wake_word_endpoint.py
│   │   ├── pyproject.toml
│   │   ├── tests/
│   │   │   └── features/
│   │   │       ├── add_device.feature
│   │   │       ├── agreements.feature
│   │   │       ├── authentication.feature
│   │   │       ├── environment.py
│   │   │       ├── pantacor_update.feature
│   │   │       ├── profile.feature
│   │   │       ├── remove_account.feature
│   │   │       └── steps/
│   │   │           ├── add_device.py
│   │   │           ├── agreements.py
│   │   │           ├── authentication.py
│   │   │           ├── common.py
│   │   │           ├── pantacor_update.py
│   │   │           ├── profile.py
│   │   │           └── remove_account.py
│   │   └── uwsgi.ini
│   ├── market/
│   │   ├── market_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── available_skills.py
│   │   │       ├── skill_detail.py
│   │   │       ├── skill_install.py
│   │   │       └── skill_install_status.py
│   │   ├── pyproject.toml
│   │   ├── swagger.yaml
│   │   └── uwsgi.ini
│   ├── precise/
│   │   ├── precise_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── audio_file.py
│   │   │       ├── designation.py
│   │   │       └── tag.py
│   │   ├── pyproject.toml
│   │   └── uwsgi.ini
│   ├── public/
│   │   ├── __init__.py
│   │   ├── public_api/
│   │   │   ├── __init__.py
│   │   │   ├── api.py
│   │   │   └── endpoints/
│   │   │       ├── __init__.py
│   │   │       ├── audio_transcription.py
│   │   │       ├── device.py
│   │   │       ├── device_activate.py
│   │   │       ├── device_code.py
│   │   │       ├── device_email.py
│   │   │       ├── device_location.py
│   │   │       ├── device_metrics.py
│   │   │       ├── device_oauth.py
│   │   │       ├── device_pantacor.py
│   │   │       ├── device_refresh_token.py
│   │   │       ├── device_setting.py
│   │   │       ├── device_skill.py
│   │   │       ├── device_skill_manifest.py
│   │   │       ├── device_skill_settings.py
│   │   │       ├── device_subscription.py
│   │   │       ├── geolocation.py
│   │   │       ├── google_stt.py
│   │   │       ├── oauth_callback.py
│   │   │       ├── open_weather_map.py
│   │   │       ├── premium_voice.py
│   │   │       ├── stripe_webhook.py
│   │   │       ├── wake_word_file.py
│   │   │       ├── wolfram_alpha.py
│   │   │       ├── wolfram_alpha_simple.py
│   │   │       ├── wolfram_alpha_spoken.py
│   │   │       └── wolfram_alpha_v2.py
│   │   ├── pyproject.toml
│   │   ├── tests/
│   │   │   └── features/
│   │   │       ├── device_email.feature
│   │   │       ├── device_location.feature
│   │   │       ├── device_metrics.feature
│   │   │       ├── device_pairing.feature
│   │   │       ├── device_refresh_token.feature
│   │   │       ├── device_skill_manifest.feature
│   │   │       ├── device_skill_settings.feature
│   │   │       ├── device_subscription.feature
│   │   │       ├── environment.py
│   │   │       ├── get_device.feature
│   │   │       ├── get_device_settings.feature
│   │   │       ├── steps/
│   │   │       │   ├── common.py
│   │   │       │   ├── device_email.py
│   │   │       │   ├── device_location.py
│   │   │       │   ├── device_metrics.py
│   │   │       │   ├── device_pairing.py
│   │   │       │   ├── device_refresh_token.py
│   │   │       │   ├── device_skill_manifest.py
│   │   │       │   ├── device_skill_settings.py
│   │   │       │   ├── get_device.py
│   │   │       │   ├── get_device_settings.py
│   │   │       │   ├── get_device_subscription.py
│   │   │       │   ├── resources/
│   │   │       │   │   └── test_stt.flac
│   │   │       │   ├── transcribe_audio.py
│   │   │       │   ├── wake_word_file.py
│   │   │       │   └── wolfram_alpha.py
│   │   │       ├── transcribe_audio.feature
│   │   │       ├── wake_word_file_upload.feature
│   │   │       └── wolfram_alpha.feature
│   │   └── uwsgi.ini
│   └── sso/
│       ├── Dockerfile
│       ├── pyproject.toml
│       ├── sso_api/
│       │   ├── __init__.py
│       │   ├── api.py
│       │   └── endpoints/
│       │       ├── __init__.py
│       │       ├── authenticate_internal.py
│       │       ├── github_token.py
│       │       ├── logout.py
│       │       ├── password_change.py
│       │       ├── password_reset.py
│       │       ├── validate_federated.py
│       │       └── validate_token.py
│       ├── tests/
│       │   └── features/
│       │       ├── add_account.feature
│       │       ├── agreements.feature
│       │       ├── environment.py
│       │       ├── federated_login.feature
│       │       ├── internal_login.feature
│       │       ├── logout.feature
│       │       ├── password_change.feature
│       │       └── steps/
│       │           ├── add_account.py
│       │           ├── agreements.py
│       │           ├── common.py
│       │           ├── login.py
│       │           ├── logout.py
│       │           └── password_change.py
│       └── uwsgi.ini
├── batch/
│   ├── job_scheduler/
│   │   ├── __init__.py
│   │   └── jobs.py
│   ├── pyproject.toml
│   └── script/
│       ├── __init__.py
│       ├── daily_report.py
│       ├── delete_wake_word_files.py
│       ├── designate_wake_word_files.py
│       ├── load_skill_display_data.py
│       ├── move_wake_word_files.py
│       ├── parse_core_metrics.py
│       ├── partition_api_metrics.py
│       ├── test_scheduler.py
│       └── update_device_last_contact.py
├── db/
│   ├── mycroft/
│   │   ├── account_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── data/
│   │   │   │   └── membership.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account.sql
│   │   │       ├── account_agreement.sql
│   │   │       ├── account_membership.sql
│   │   │       ├── agreement.sql
│   │   │       └── membership.sql
│   │   ├── create_extensions.sql
│   │   ├── create_mycroft_db.sql
│   │   ├── create_roles.sql
│   │   ├── create_template_db.sql
│   │   ├── device_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── data/
│   │   │   │   └── text_to_speech.sql
│   │   │   ├── get_device_defaults_for_city.sql
│   │   │   ├── get_device_geographies_for_city.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account_defaults.sql
│   │   │       ├── account_preferences.sql
│   │   │       ├── category.sql
│   │   │       ├── device.sql
│   │   │       ├── device_skill.sql
│   │   │       ├── geography.sql
│   │   │       ├── pantacor_config.sql
│   │   │       ├── skill_setting.sql
│   │   │       ├── text_to_speech.sql
│   │   │       ├── wake_word.sql
│   │   │       └── wake_word_settings.sql
│   │   ├── drop_extensions.sql
│   │   ├── drop_mycroft_db.sql
│   │   ├── drop_roles.sql
│   │   ├── drop_template_db.sql
│   │   ├── geography_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── delete_duplicate_cities.sql
│   │   │   ├── get_duplicated_cities.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── city.sql
│   │   │       ├── country.sql
│   │   │       ├── region.sql
│   │   │       └── timezone.sql
│   │   ├── metric_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── account_activity.sql
│   │   │       ├── api.sql
│   │   │       ├── api_history.sql
│   │   │       ├── core.sql
│   │   │       ├── core_interaction.sql
│   │   │       ├── job.sql
│   │   │       ├── stt_engine.sql
│   │   │       └── stt_transcription.sql
│   │   ├── skill_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── display.sql
│   │   │       ├── oauth_credential.sql
│   │   │       ├── oauth_token.sql
│   │   │       ├── settings_display.sql
│   │   │       └── skill.sql
│   │   ├── tagging_schema/
│   │   │   ├── create_schema.sql
│   │   │   ├── grants.sql
│   │   │   └── tables/
│   │   │       ├── file_location.sql
│   │   │       ├── session.sql
│   │   │       ├── tag.sql
│   │   │       ├── tag_value.sql
│   │   │       ├── tagger.sql
│   │   │       ├── wake_word_file.sql
│   │   │       ├── wake_word_file_designation.sql
│   │   │       └── wake_word_file_tag.sql
│   │   ├── types/
│   │   │   ├── agreement_enum.sql
│   │   │   ├── cateogory_enum.sql
│   │   │   ├── core_version_enum.sql
│   │   │   ├── date_format_enum.sql
│   │   │   ├── measurement_system_enum.sql
│   │   │   ├── membership_type_enum.sql
│   │   │   ├── payment_method_enum.sql
│   │   │   ├── tagger_type_enum.sql
│   │   │   ├── tagging_file_origin_enum.sql
│   │   │   ├── tagging_file_status_enum.sql
│   │   │   ├── time_format_enum.sql
│   │   │   └── tts_engine_enum.sql
│   │   ├── versions/
│   │   │   └── 2020.9.1.sql
│   │   └── wake_word_schema/
│   │       ├── create_schema.sql
│   │       ├── grants.sql
│   │       └── tables/
│   │           ├── pocketsphinx_settings.sql
│   │           └── wake_word.sql
│   ├── pyproject.toml
│   └── scripts/
│       ├── __init__.py
│       ├── bootstrap_mycroft_db.py
│       ├── neo4j-postgres.py
│       ├── queries.cypher
│       └── remove_duplicate_cities.py
└── shared/
    ├── Dockerfile
    ├── MANIFEST.in
    ├── pyproject.toml
    ├── selene/
    │   ├── __init__.py
    │   ├── api/
    │   │   ├── __init__.py
    │   │   ├── base_config.py
    │   │   ├── base_endpoint.py
    │   │   ├── blueprint.py
    │   │   ├── endpoints/
    │   │   │   ├── __init__.py
    │   │   │   ├── account.py
    │   │   │   ├── agreements.py
    │   │   │   ├── password_change.py
    │   │   │   └── validate_email.py
    │   │   ├── etag.py
    │   │   ├── pantacor.py
    │   │   ├── public_endpoint.py
    │   │   └── response.py
    │   ├── batch/
    │   │   ├── __init__.py
    │   │   └── base.py
    │   ├── data/
    │   │   ├── __init__.py
    │   │   ├── account/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── account.py
    │   │   │   │   ├── agreement.py
    │   │   │   │   ├── membership.py
    │   │   │   │   └── skill.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── account.py
    │   │   │       ├── agreement.py
    │   │   │       ├── membership.py
    │   │   │       ├── skill.py
    │   │   │       └── sql/
    │   │   │           ├── add_account.sql
    │   │   │           ├── add_account_agreement.sql
    │   │   │           ├── add_account_membership.sql
    │   │   │           ├── add_agreement.sql
    │   │   │           ├── add_membership.sql
    │   │   │           ├── change_email_address.sql
    │   │   │           ├── change_password.sql
    │   │   │           ├── daily_report.sql
    │   │   │           ├── delete_agreement.sql
    │   │   │           ├── delete_membership.sql
    │   │   │           ├── end_membership.sql
    │   │   │           ├── expire_account_agreement.sql
    │   │   │           ├── expire_agreement.sql
    │   │   │           ├── get_account.sql
    │   │   │           ├── get_account_by_device_id.sql
    │   │   │           ├── get_account_skills.sql
    │   │   │           ├── get_active_membership_by_account_id.sql
    │   │   │           ├── get_active_membership_by_payment_account_id.sql
    │   │   │           ├── get_agreement_content_id.sql
    │   │   │           ├── get_current_agreements.sql
    │   │   │           ├── get_membership_by_type.sql
    │   │   │           ├── get_membership_types.sql
    │   │   │           ├── remove_account.sql
    │   │   │           ├── update_last_activity_ts.sql
    │   │   │           └── update_username.sql
    │   │   ├── device/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── default.py
    │   │   │   │   ├── device.py
    │   │   │   │   ├── device_skill.py
    │   │   │   │   ├── geography.py
    │   │   │   │   ├── preference.py
    │   │   │   │   └── text_to_speech.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── default.py
    │   │   │       ├── device.py
    │   │   │       ├── device_skill.py
    │   │   │       ├── geography.py
    │   │   │       ├── preference.py
    │   │   │       ├── setting.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_device.sql
    │   │   │       │   ├── add_geography.sql
    │   │   │       │   ├── add_manifest_skill.sql
    │   │   │       │   ├── add_text_to_speech.sql
    │   │   │       │   ├── delete_device_skill.sql
    │   │   │       │   ├── get_account_defaults.sql
    │   │   │       │   ├── get_account_device_count.sql
    │   │   │       │   ├── get_account_geographies.sql
    │   │   │       │   ├── get_account_preferences.sql
    │   │   │       │   ├── get_all_device_ids.sql
    │   │   │       │   ├── get_device_by_id.sql
    │   │   │       │   ├── get_device_settings_by_device_id.sql
    │   │   │       │   ├── get_device_skill_manifest.sql
    │   │   │       │   ├── get_devices_by_account_id.sql
    │   │   │       │   ├── get_location_by_device_id.sql
    │   │   │       │   ├── get_open_dataset_agreement_by_device_id.sql
    │   │   │       │   ├── get_settings_display_usage.sql
    │   │   │       │   ├── get_skill_manifest_for_account.sql
    │   │   │       │   ├── get_skill_settings_for_account.sql
    │   │   │       │   ├── get_skill_settings_for_device.sql
    │   │   │       │   ├── get_voices.sql
    │   │   │       │   ├── remove_device.sql
    │   │   │       │   ├── remove_manifest_skill.sql
    │   │   │       │   ├── remove_text_to_speech.sql
    │   │   │       │   ├── update_device_from_account.sql
    │   │   │       │   ├── update_device_from_core.sql
    │   │   │       │   ├── update_device_skill_settings.sql
    │   │   │       │   ├── update_last_contact_ts.sql
    │   │   │       │   ├── update_pantacor_config.sql
    │   │   │       │   ├── update_skill_manifest.sql
    │   │   │       │   ├── update_skill_settings.sql
    │   │   │       │   ├── upsert_defaults.sql
    │   │   │       │   ├── upsert_device_skill_settings.sql
    │   │   │       │   ├── upsert_pantacor_config.sql
    │   │   │       │   └── upsert_preferences.sql
    │   │   │       └── text_to_speech.py
    │   │   ├── geography/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── city.py
    │   │   │   │   ├── country.py
    │   │   │   │   ├── region.py
    │   │   │   │   └── timezone.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── city.py
    │   │   │       ├── country.py
    │   │   │       ├── region.py
    │   │   │       ├── sql/
    │   │   │       │   ├── get_biggest_city_in_country.sql
    │   │   │       │   ├── get_biggest_city_in_region.sql
    │   │   │       │   ├── get_cities_by_region.sql
    │   │   │       │   ├── get_countries.sql
    │   │   │       │   ├── get_geographic_location_by_city.sql
    │   │   │       │   ├── get_regions_by_country.sql
    │   │   │       │   └── get_timezones_by_country.sql
    │   │   │       └── timezone.py
    │   │   ├── metric/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── account_activity.py
    │   │   │   │   ├── api.py
    │   │   │   │   ├── core.py
    │   │   │   │   ├── job.py
    │   │   │   │   └── stt.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── account_activity.py
    │   │   │       ├── api.py
    │   │   │       ├── core.py
    │   │   │       ├── job.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_account_activity.sql
    │   │   │       │   ├── add_api_metric.sql
    │   │   │       │   ├── add_core_interaction.sql
    │   │   │       │   ├── add_core_metric.sql
    │   │   │       │   ├── add_job_metric.sql
    │   │   │       │   ├── add_tts_transcription_metric.sql
    │   │   │       │   ├── create_api_metric_partition.sql
    │   │   │       │   ├── create_api_metric_partition_index.sql
    │   │   │       │   ├── delete_account_activity_date.sql
    │   │   │       │   ├── delete_api_metrics_by_date.sql
    │   │   │       │   ├── delete_stt_transcription_by_date.sql
    │   │   │       │   ├── get_account_activity_by_date.sql
    │   │   │       │   ├── get_api_metrics_for_date.sql
    │   │   │       │   ├── get_core_metric_by_device.sql
    │   │   │       │   ├── get_core_timing_metrics_by_date.sql
    │   │   │       │   ├── get_tts_transcription_by_account.sql
    │   │   │       │   ├── increment_accounts_added.sql
    │   │   │       │   ├── increment_accounts_deleted.sql
    │   │   │       │   ├── increment_activity.sql
    │   │   │       │   ├── increment_members_added.sql
    │   │   │       │   ├── increment_members_expired.sql
    │   │   │       │   ├── increment_open_dataset_added.sql
    │   │   │       │   └── increment_open_dataset_deleted.sql
    │   │   │       └── stt.py
    │   │   ├── repository_base.py
    │   │   ├── skill/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── display.py
    │   │   │   │   ├── skill.py
    │   │   │   │   └── skill_setting.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── display.py
    │   │   │       ├── setting.py
    │   │   │       ├── settings_display.py
    │   │   │       ├── skill.py
    │   │   │       └── sql/
    │   │   │           ├── add_device_skill.sql
    │   │   │           ├── add_settings_display.sql
    │   │   │           ├── add_skill.sql
    │   │   │           ├── delete_device_skill.sql
    │   │   │           ├── delete_settings_display.sql
    │   │   │           ├── get_display_data_for_skill.sql
    │   │   │           ├── get_display_data_for_skills.sql
    │   │   │           ├── get_settings_definition_by_gid.sql
    │   │   │           ├── get_settings_display_id.sql
    │   │   │           ├── get_settings_for_skill_family.sql
    │   │   │           ├── get_skill_by_global_id.sql
    │   │   │           ├── get_skill_setting_by_device.sql
    │   │   │           ├── get_skills_for_account.sql
    │   │   │           ├── remove_skill_by_gid.sql
    │   │   │           ├── update_device_skill_settings.sql
    │   │   │           └── upsert_skill_display_data.sql
    │   │   ├── tagging/
    │   │   │   ├── __init__.py
    │   │   │   ├── entity/
    │   │   │   │   ├── __init__.py
    │   │   │   │   ├── file_designation.py
    │   │   │   │   ├── file_location.py
    │   │   │   │   ├── file_tag.py
    │   │   │   │   ├── tag.py
    │   │   │   │   ├── tag_value.py
    │   │   │   │   ├── tagger.py
    │   │   │   │   └── wake_word_file.py
    │   │   │   └── repository/
    │   │   │       ├── __init__.py
    │   │   │       ├── file_designation.py
    │   │   │       ├── file_location.py
    │   │   │       ├── file_tag.py
    │   │   │       ├── session.py
    │   │   │       ├── sql/
    │   │   │       │   ├── add_file_location.sql
    │   │   │       │   ├── add_session.sql
    │   │   │       │   ├── add_tagger.sql
    │   │   │       │   ├── add_tagging_session.sql
    │   │   │       │   ├── add_wake_word_file.sql
    │   │   │       │   ├── add_wake_word_file_designation.sql
    │   │   │       │   ├── add_wake_word_file_tag.sql
    │   │   │       │   ├── change_account_file_status.sql
    │   │   │       │   ├── change_file_location.sql
    │   │   │       │   ├── change_file_status.sql
    │   │   │       │   ├── get_active_session.sql
    │   │   │       │   ├── get_designation_candidates.sql
    │   │   │       │   ├── get_designations_from_date.sql
    │   │   │       │   ├── get_file_location_id.sql
    │   │   │       │   ├── get_taggable_wake_word_file.sql
    │   │   │       │   ├── get_tagger_by_entity.sql
    │   │   │       │   ├── get_tags.sql
    │   │   │       │   ├── get_wake_word_files.sql
    │   │   │       │   ├── remove_file_location.sql
    │   │   │       │   ├── remove_wake_word_file.sql
    │   │   │       │   └── update_session_end_ts.sql
    │   │   │       ├── tag.py
    │   │   │       ├── tagger.py
    │   │   │       └── wake_word_file.py
    │   │   └── wake_word/
    │   │       ├── __init__.py
    │   │       ├── entity/
    │   │       │   ├── __init__.py
    │   │       │   ├── pocketsphinx_settings.py
    │   │       │   └── wake_word.py
    │   │       └── repository/
    │   │           ├── __init__.py
    │   │           ├── sql/
    │   │           │   ├── add_wake_word.sql
    │   │           │   ├── get_wake_word_id.sql
    │   │           │   ├── get_wake_words_for_web.sql
    │   │           │   └── remove_wake_word.sql
    │   │           └── wake_word.py
    │   ├── testing/
    │   │   ├── __init__.py
    │   │   ├── account.py
    │   │   ├── account_activity.py
    │   │   ├── account_geography.py
    │   │   ├── account_preference.py
    │   │   ├── agreement.py
    │   │   ├── api.py
    │   │   ├── device.py
    │   │   ├── device_skill.py
    │   │   ├── membership.py
    │   │   ├── skill.py
    │   │   ├── tagging.py
    │   │   ├── test_db.py
    │   │   ├── text_to_speech.py
    │   │   └── wake_word.py
    │   └── util/
    │       ├── __init__.py
    │       ├── auth.py
    │       ├── cache.py
    │       ├── db/
    │       │   ├── __init__.py
    │       │   ├── connection.py
    │       │   ├── connection_pool.py
    │       │   ├── cursor.py
    │       │   └── transaction.py
    │       ├── email/
    │       │   ├── __init__.py
    │       │   ├── email.py
    │       │   └── templates/
    │       │       ├── account_not_found.html
    │       │       ├── base.html
    │       │       ├── email_change.html
    │       │       ├── email_verification.html
    │       │       ├── metrics.html
    │       │       ├── password_change.html
    │       │       └── reset_password.html
    │       ├── exceptions.py
    │       ├── github.py
    │       ├── log.py
    │       ├── payment/
    │       │   ├── __init__.py
    │       │   └── stripe.py
    │       └── ssh/
    │           ├── __init__.py
    │           ├── sftp.py
    │           └── ssh.py
    └── setup.py
Download .txt
SYMBOL INDEX (1296 symbols across 254 files)

FILE: api/account/account_api/endpoints/change_email_address.py
  class EmailAddressChangeEndpoint (line 34) | class EmailAddressChangeEndpoint(SeleneEndpoint):
    method put (line 37) | def put(self):
    method _validate_request (line 46) | def _validate_request(self) -> str:
    method _send_notification (line 60) | def _send_notification(self):
    method _send_verification_email (line 74) | def _send_verification_email(new_email_address):

FILE: api/account/account_api/endpoints/change_password.py
  class PasswordChangeEndpoint (line 25) | class PasswordChangeEndpoint(CommonPasswordChangeEndpoint):
    method account_id (line 29) | def account_id(self):
    method _send_email (line 32) | def _send_email(self):

FILE: api/account/account_api/endpoints/city.py
  class CityEndpoint (line 26) | class CityEndpoint(SeleneEndpoint):
    method get (line 29) | def get(self):

FILE: api/account/account_api/endpoints/country.py
  class CountryEndpoint (line 26) | class CountryEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/account/account_api/endpoints/defaults.py
  class DefaultsRequest (line 32) | class DefaultsRequest(Model):
  class AccountDefaultsEndpoint (line 43) | class AccountDefaultsEndpoint(SeleneEndpoint):
    method __init__ (line 46) | def __init__(self):
    method get (line 50) | def get(self):
    method _get_defaults (line 63) | def _get_defaults(self):
    method post (line 70) | def post(self):
    method patch (line 78) | def patch(self):
    method _validate_request (line 86) | def _validate_request(self) -> dict:
    method _upsert_defaults (line 100) | def _upsert_defaults(self, defaults: dict):

FILE: api/account/account_api/endpoints/device.py
  function validate_pairing_code (line 52) | def validate_pairing_code(pairing_code):
  class UpdateDeviceRequest (line 62) | class UpdateDeviceRequest(Model):
  class NewDeviceRequest (line 78) | class NewDeviceRequest(UpdateDeviceRequest):
  class DeviceEndpoint (line 88) | class DeviceEndpoint(SeleneEndpoint):
    method __init__ (line 93) | def __init__(self):
    method device_repository (line 108) | def device_repository(self):
    method get (line 115) | def get(self, device_id: str):
    method _get_devices (line 125) | def _get_devices(self) -> List[dict]:
    method _get_device (line 138) | def _get_device(self, device_id: str) -> dict:
    method _format_device_for_response (line 149) | def _format_device_for_response(self, device: Device) -> dict:
    method _format_pantacor_config (line 168) | def _format_pantacor_config(self, config) -> dict[str, str]:
    method _format_device_status (line 187) | def _format_device_status(self, device: Device) -> tuple[str, Optional...
    method _get_device_last_contact (line 202) | def _get_device_last_contact(self, device: Device) -> timedelta:
    method _determine_device_status (line 233) | def _determine_device_status(last_contact_age: timedelta) -> str:
    method _determine_disconnect_duration (line 249) | def _determine_disconnect_duration(last_contact_age: timedelta) -> str:
    method post (line 270) | def post(self):
    method _pair_device (line 279) | def _pair_device(self):
    method _get_pairing_data (line 290) | def _get_pairing_data(self, cache_key) -> dict:
    method _add_device (line 300) | def _add_device(self) -> str:
    method _build_pairing_token (line 310) | def _build_pairing_token(self, pairing_data: dict):
    method delete (line 321) | def delete(self, device_id: str):
    method _delete_device (line 331) | def _delete_device(self, device_id: str):
    method patch (line 342) | def patch(self, device_id: str):
    method _validate_request (line 356) | def _validate_request(self):
    method _ensure_geography_exists (line 372) | def _ensure_geography_exists(self):
    method _update_device (line 391) | def _update_device(self, device_id: str):
    method _update_pantacor_config (line 408) | def _update_pantacor_config(self, device: Device):
    method _convert_release_channel (line 426) | def _convert_release_channel(self, release_channel: str) -> str:

FILE: api/account/account_api/endpoints/device_count.py
  class DeviceCountEndpoint (line 25) | class DeviceCountEndpoint(SeleneEndpoint):
    method get (line 26) | def get(self):
    method _get_devices (line 32) | def _get_devices(self):

FILE: api/account/account_api/endpoints/geography.py
  class GeographyEndpoint (line 26) | class GeographyEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):
    method _build_response_data (line 33) | def _build_response_data(self):

FILE: api/account/account_api/endpoints/membership.py
  class MembershipEndpoint (line 26) | class MembershipEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/account/account_api/endpoints/pairing_code.py
  class PairingCodeEndpoint (line 25) | class PairingCodeEndpoint(SeleneEndpoint):
    method __init__ (line 26) | def __init__(self):
    method get (line 30) | def get(self, pairing_code):
    method _get_pairing_data (line 36) | def _get_pairing_data(self, pairing_code: str) -> bool:

FILE: api/account/account_api/endpoints/preferences.py
  class PreferencesRequest (line 31) | class PreferencesRequest(Model):
  class PreferencesEndpoint (line 37) | class PreferencesEndpoint(SeleneEndpoint):
    method __init__ (line 38) | def __init__(self):
    method get (line 44) | def get(self):
    method _get_preferences (line 56) | def _get_preferences(self):
    method post (line 60) | def post(self):
    method patch (line 67) | def patch(self):
    method _validate_request (line 74) | def _validate_request(self):
    method _upsert_preferences (line 81) | def _upsert_preferences(self):

FILE: api/account/account_api/endpoints/region.py
  class RegionEndpoint (line 26) | class RegionEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/account/account_api/endpoints/skill_oauth.py
  class SkillOauthEndpoint (line 27) | class SkillOauthEndpoint(SeleneEndpoint):
    method __init__ (line 28) | def __init__(self):
    method get (line 32) | def get(self, oauth_id):
    method _get_oauth_url (line 36) | def _get_oauth_url(self, oauth_id):

FILE: api/account/account_api/endpoints/skill_settings.py
  class SkillSettingsEndpoint (line 30) | class SkillSettingsEndpoint(SeleneEndpoint):
    method __init__ (line 33) | def __init__(self):
    method setting_repository (line 42) | def setting_repository(self):
    method get (line 49) | def get(self, skill_family_name):
    method _parse_selection_options (line 66) | def _parse_selection_options(self):
    method _build_response_data (line 87) | def _build_response_data(self):
    method put (line 104) | def put(self, skill_family_name):
    method _update_settings_values (line 111) | def _update_settings_values(self):

FILE: api/account/account_api/endpoints/skills.py
  class SkillsEndpoint (line 26) | class SkillsEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):
    method _build_response_data (line 33) | def _build_response_data(self):

FILE: api/account/account_api/endpoints/software_update.py
  class SoftwareUpdateRequest (line 29) | class SoftwareUpdateRequest(Model):
  class SoftwareUpdateEndpoint (line 35) | class SoftwareUpdateEndpoint(SeleneEndpoint):
    method patch (line 38) | def patch(self):
    method _validate_request (line 45) | def _validate_request(self):

FILE: api/account/account_api/endpoints/ssh_key_validator.py
  class SshKeyValidatorEndpoint (line 27) | class SshKeyValidatorEndpoint(SeleneEndpoint):
    method get (line 30) | def get(self):

FILE: api/account/account_api/endpoints/timezone.py
  class TimezoneEndpoint (line 26) | class TimezoneEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/account/account_api/endpoints/verify_email_address.py
  class VerifyEmailAddressEndpoint (line 30) | class VerifyEmailAddressEndpoint(SeleneEndpoint):
    method put (line 33) | def put(self):
    method _validate_email_address (line 41) | def _validate_email_address(self) -> str:
    method _update_account (line 59) | def _update_account(self, email_address: str):

FILE: api/account/account_api/endpoints/voice_endpoint.py
  class VoiceEndpoint (line 26) | class VoiceEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/account/account_api/endpoints/wake_word_endpoint.py
  class WakeWordEndpoint (line 27) | class WakeWordEndpoint(SeleneEndpoint):
    method get (line 30) | def get(self):
    method _build_response_data (line 37) | def _build_response_data(self):

FILE: api/account/tests/features/environment.py
  function acct_api_client (line 37) | def acct_api_client(context):
  function before_all (line 46) | def before_all(context):
  function after_all (line 58) | def after_all(context):
  function before_scenario (line 70) | def before_scenario(context, _):
  function after_scenario (line 82) | def after_scenario(context, _):
  function _clean_cache (line 97) | def _clean_cache():

FILE: api/account/tests/features/steps/add_device.py
  function set_device_pairing_code (line 35) | def set_device_pairing_code(context):
  function add_device (line 53) | def add_device(context):
  function validate_pairing_code_removal (line 73) | def validate_pairing_code_removal(context):
  function validate_response (line 83) | def validate_response(context):
  function validate_pairing_token (line 101) | def validate_pairing_token(context):

FILE: api/account/tests/features/steps/agreements.py
  function call_agreement_endpoint (line 30) | def call_agreement_endpoint(context, agreement):
  function validate_response (line 42) | def validate_response(context, agreement, version):

FILE: api/account/tests/features/steps/authentication.py
  function use_account_with_valid_access_token (line 36) | def use_account_with_valid_access_token(context):
  function generate_expired_access_token (line 45) | def generate_expired_access_token(context):
  function generate_refresh_token_only (line 55) | def generate_refresh_token_only(context):
  function expire_both_tokens (line 63) | def expire_both_tokens(context):
  function check_for_no_new_cookie (line 72) | def check_for_no_new_cookie(context):
  function check_for_new_cookies (line 78) | def check_for_new_cookies(context):

FILE: api/account/tests/features/steps/common.py
  function define_account (line 34) | def define_account(context):
  function use_account_with_valid_access_token (line 39) | def use_account_with_valid_access_token(context):
  function check_request_success (line 47) | def check_request_success(context):
  function check_for_bad_request (line 54) | def check_for_bad_request(context, error_type):

FILE: api/account/tests/features/steps/pantacor_update.py
  function add_pantacor_device (line 31) | def add_pantacor_device(context):
  function add_pantacor_deployment_id (line 40) | def add_pantacor_deployment_id(context):
  function apply_software_update (line 46) | def apply_software_update(context):
  function validate_invalid_ssh_key (line 67) | def validate_invalid_ssh_key(context):
  function validate_valid_ssh_key (line 76) | def validate_valid_ssh_key(context):
  function get_device (line 86) | def get_device(context):
  function check_for_deployment_id (line 106) | def check_for_deployment_id(context):
  function check_for_malformed_ssh_key (line 116) | def check_for_malformed_ssh_key(context):
  function check_for_well_formed_ssh_key (line 123) | def check_for_well_formed_ssh_key(context):

FILE: api/account/tests/features/steps/profile.py
  function add_membership_to_account (line 62) | def add_membership_to_account(context):
  function get_account_no_membership (line 81) | def get_account_no_membership(context):
  function set_account_open_dataset (line 87) | def set_account_open_dataset(context, in_or_out):
  function setup_user (line 97) | def setup_user(context):
  function call_password_change_endpoint (line 104) | def call_password_change_endpoint(context):
  function call_email_address_change_endpoint (line 120) | def call_email_address_change_endpoint(context):
  function call_email_validation_endpoint (line 139) | def call_email_validation_endpoint(context):
  function call_account_endpoint (line 153) | def call_account_endpoint(context):
  function add_monthly_membership (line 161) | def add_monthly_membership(context):
  function cancel_membership (line 167) | def cancel_membership(context):
  function _add_membership_via_api (line 177) | def _add_membership_via_api(context):
  function change_to_yearly_account (line 193) | def change_to_yearly_account(context):
  function set_open_dataset_status (line 204) | def set_open_dataset_status(context, in_or_out):
  function validate_response (line 216) | def validate_response(context):
  function validate_monthly_account (line 238) | def validate_monthly_account(context):
  function validate_absence_of_membership (line 251) | def validate_absence_of_membership(context):
  function yearly_account (line 261) | def yearly_account(context):
  function check_new_member_account_metrics (line 272) | def check_new_member_account_metrics(context):
  function check_expired_member_account_metrics (line 278) | def check_expired_member_account_metrics(context):
  function check_for_open_dataset_agreement (line 301) | def check_for_open_dataset_agreement(context, will_or_wont):
  function check_new_open_dataset_account_metrics (line 315) | def check_new_open_dataset_account_metrics(context):
  function check_deleted_open_dataset_account_metrics (line 321) | def check_deleted_open_dataset_account_metrics(context):
  function check_new_password (line 327) | def check_new_password(context):
  function check_for_duplicate_account_error (line 338) | def check_for_duplicate_account_error(context):
  function check_password_change_notification_sent (line 345) | def check_password_change_notification_sent(context):
  function check_email_change_notification_sent (line 362) | def check_email_change_notification_sent(context):
  function check_new_email_verification_sent (line 379) | def check_new_email_verification_sent(context):

FILE: api/account/tests/features/steps/remove_account.py
  function add_wake_word_sample (line 40) | def add_wake_word_sample(context):
  function call_account_endpoint (line 62) | def call_account_endpoint(context):
  function account_deleted (line 68) | def account_deleted(context):
  function check_stripe (line 77) | def check_stripe(context):
  function check_db_for_account_metrics (line 90) | def check_db_for_account_metrics(context):
  function check_wake_word_file_status (line 106) | def check_wake_word_file_status(context):

FILE: api/market/market_api/endpoints/available_skills.py
  class AvailableSkillsEndpoint (line 32) | class AvailableSkillsEndpoint(SeleneEndpoint):
    method __init__ (line 37) | def __init__(self):
    method get (line 43) | def get(self):
    method _get_available_skills (line 51) | def _get_available_skills(self):
    method _build_response_data (line 61) | def _build_response_data(self):
    method _filter_skills (line 70) | def _filter_skills(self) -> list:
    method _reformat_skills (line 92) | def _reformat_skills(self, skills_to_include: List[SkillDisplay]):
    method _sort_skills (line 122) | def _sort_skills(self):

FILE: api/market/market_api/endpoints/skill_detail.py
  class SkillDetailEndpoint (line 29) | class SkillDetailEndpoint(SeleneEndpoint):
    method __init__ (line 34) | def __init__(self):
    method get (line 40) | def get(self, skill_display_id):
    method _get_skill_details (line 49) | def _get_skill_details(self) -> SkillDisplay:
    method _build_response_data (line 58) | def _build_response_data(self, skill_display: SkillDisplay):

FILE: api/market/market_api/endpoints/skill_install.py
  class InstallRequest (line 47) | class InstallRequest(Model):
  class SkillInstallEndpoint (line 56) | class SkillInstallEndpoint(SeleneEndpoint):
    method __init__ (line 61) | def __init__(self):
    method settings_repo (line 68) | def settings_repo(self):
    method put (line 75) | def put(self):
    method _validate_request (line 88) | def _validate_request(self):
    method _get_skill_name (line 98) | def _get_skill_name(self):
    method _apply_update (line 110) | def _apply_update(self):
    method _update_skill_settings (line 126) | def _update_skill_settings(self, new_skill_settings):

FILE: api/market/market_api/endpoints/skill_install_status.py
  class SkillInstallStatusEndpoint (line 32) | class SkillInstallStatusEndpoint(SeleneEndpoint):
    method __init__ (line 35) | def __init__(self):
    method get (line 39) | def get(self):
    method _get_installed_skills (line 51) | def _get_installed_skills(self):
    method _build_response_data (line 57) | def _build_response_data(self) -> dict:
  class SkillManifestAggregator (line 72) | class SkillManifestAggregator(object):
    method __init__ (line 75) | def __init__(self, installed_skills: List[ManifestSkill]):
    method aggregate_skill_status (line 79) | def aggregate_skill_status(self):
    method _validate_install_status (line 90) | def _validate_install_status(self):
    method _determine_install_status (line 100) | def _determine_install_status(self):
    method _determine_failure_reason (line 126) | def _determine_failure_reason(self):

FILE: api/precise/precise_api/endpoints/audio_file.py
  class AudioFileEndpoint (line 28) | class AudioFileEndpoint(SeleneEndpoint):
    method get (line 31) | def get(self, file_name):

FILE: api/precise/precise_api/endpoints/designation.py
  class DesignationEndpoint (line 41) | class DesignationEndpoint(SeleneEndpoint):
    method tags (line 52) | def tags(self) -> List[Tag]:
    method get (line 64) | def get(self):
    method _build_response_data (line 70) | def _build_response_data(self):
    method _get_designations (line 90) | def _get_designations(self) -> List[FileDesignation]:
    method _include_in_result (line 99) | def _include_in_result(self, tag: Tag, tag_value: TagValue) -> bool:
    method _get_tag (line 117) | def _get_tag(self, designation: FileDesignation) -> Tag:
    method _get_tag_value (line 131) | def _get_tag_value(designation: FileDesignation, tag: Tag):

FILE: api/precise/precise_api/endpoints/tag.py
  class TagPostRequest (line 46) | class TagPostRequest(Model):
  class TagEndpoint (line 55) | class TagEndpoint(SeleneEndpoint):
    method tags (line 68) | def tags(self) -> List[Tag]:
    method get (line 80) | def get(self):
    method _ensure_session_exists (line 90) | def _ensure_session_exists(self):
    method _ensure_tagger_exists (line 100) | def _ensure_tagger_exists(self):
    method _build_response_data (line 108) | def _build_response_data(self, session_id: str):
    method _get_taggable_file (line 133) | def _get_taggable_file(self, wake_word: str, session_id: str) -> Tagga...
    method _select_tag (line 147) | def _select_tag(self, file_to_tag: TaggableFile) -> Tag:
    method _copy_audio_file (line 169) | def _copy_audio_file(file_to_tag: TaggableFile):
    method post (line 193) | def post(self):
    method _validate_post_request (line 201) | def _validate_post_request(self):
    method _add_tag (line 213) | def _add_tag(self):

FILE: api/public/public_api/endpoints/audio_transcription.py
  class AudioTranscriptionEndpoint (line 44) | class AudioTranscriptionEndpoint(PublicEndpoint):
    method __init__ (line 47) | def __init__(self):
    method post (line 52) | def post(self):
    method _transcribe (line 62) | def _transcribe(self) -> Optional[str]:
    method _call_transcription_api (line 72) | def _call_transcription_api(self) -> Optional[speech.RecognizeResponse]:
    method _get_transcription (line 98) | def _get_transcription(
    method _add_transcription_metric (line 116) | def _add_transcription_metric(self, transcription: str):
    method _determine_audio_duration (line 130) | def _determine_audio_duration(self) -> float:

FILE: api/public/public_api/endpoints/device.py
  class UpdateDevice (line 31) | class UpdateDevice(Model):
  class DeviceEndpoint (line 38) | class DeviceEndpoint(PublicEndpoint):
    method __init__ (line 41) | def __init__(self):
    method get (line 44) | def get(self, device_id):
    method patch (line 67) | def patch(self, device_id):

FILE: api/public/public_api/endpoints/device_activate.py
  class ActivationRequest (line 40) | class ActivationRequest(Model):
  class DeviceActivateEndpoint (line 52) | class DeviceActivateEndpoint(PublicEndpoint):
    method device_repository (line 58) | def device_repository(self):
    method post (line 65) | def post(self):
    method _validate_request (line 78) | def _validate_request(self) -> dict:
    method _get_pairing_session (line 94) | def _get_pairing_session(self):
    method _activate (line 112) | def _activate(self, device_id: str, activation_request: dict):

FILE: api/public/public_api/endpoints/device_code.py
  class DeviceCodeEndpoint (line 53) | class DeviceCodeEndpoint(PublicEndpoint):
    method get (line 56) | def get(self):
    method _build_response (line 66) | def _build_response(self):
    method _generate_token (line 88) | def _generate_token():
    method _generate_pairing_code (line 96) | def _generate_pairing_code():
    method _add_pairing_code_to_cache (line 103) | def _add_pairing_code_to_cache(self, response_data):

FILE: api/public/public_api/endpoints/device_email.py
  class SendEmail (line 30) | class SendEmail(Model):
  class DeviceEmailEndpoint (line 38) | class DeviceEmailEndpoint(PublicEndpoint):
    method put (line 41) | def put(self, device_id):
    method _validate_request (line 50) | def _validate_request(self):
    method _send_message (line 55) | def _send_message(self, account):

FILE: api/public/public_api/endpoints/device_location.py
  class DeviceLocationEndpoint (line 27) | class DeviceLocationEndpoint(PublicEndpoint):
    method __init__ (line 28) | def __init__(self):
    method get (line 31) | def get(self, device_id):

FILE: api/public/public_api/endpoints/device_metrics.py
  class DeviceMetricsEndpoint (line 25) | class DeviceMetricsEndpoint(PublicEndpoint):
    method post (line 28) | def post(self, device_id, metric):
    method _add_core_metric (line 35) | def _add_core_metric(self, metric: str):

FILE: api/public/public_api/endpoints/device_oauth.py
  class OauthServiceEndpoint (line 28) | class OauthServiceEndpoint(PublicEndpoint):
    method __init__ (line 29) | def __init__(self):
    method get (line 33) | def get(self, device_id, credentials, oauth_path):

FILE: api/public/public_api/endpoints/device_pantacor.py
  class PantacorSyncRequest (line 40) | class PantacorSyncRequest(Model):
  class DevicePantacorEndpoint (line 47) | class DevicePantacorEndpoint(PublicEndpoint):
    method post (line 57) | def post(self):
    method _validate_request (line 74) | def _validate_request(self):
    method _get_config_from_pantacor (line 80) | def _get_config_from_pantacor(self):
    method _add_pantacor_config_to_db (line 92) | def _add_pantacor_config_to_db(self, pantacor_config):

FILE: api/public/public_api/endpoints/device_refresh_token.py
  class DeviceRefreshTokenEndpoint (line 28) | class DeviceRefreshTokenEndpoint(PublicEndpoint):
    method __init__ (line 32) | def __init__(self):
    method get (line 36) | def get(self):
    method _refresh_session_token (line 62) | def _refresh_session_token(self, refresh: str):
    method _refresh_session_token_device (line 71) | def _refresh_session_token_device(self, device: str):

FILE: api/public/public_api/endpoints/device_setting.py
  class DeviceSettingEndpoint (line 26) | class DeviceSettingEndpoint(PublicEndpoint):
    method __init__ (line 29) | def __init__(self):
    method get (line 32) | def get(self, device_id):

FILE: api/public/public_api/endpoints/device_skill.py
  function _normalize_field_value (line 52) | def _normalize_field_value(field):
  class RequestSkillField (line 73) | class RequestSkillField(Model):
  class RequestSkillSection (line 86) | class RequestSkillSection(Model):
  class RequestSkillMetadata (line 93) | class RequestSkillMetadata(Model):
  class RequestSkillIcon (line 99) | class RequestSkillIcon(Model):
  class RequestDeviceSkill (line 106) | class RequestDeviceSkill(Model):
  class SkillSettingsMetaEndpoint (line 116) | class SkillSettingsMetaEndpoint(PublicEndpoint):
    method __init__ (line 119) | def __init__(self):
    method device_skill_repo (line 128) | def device_skill_repo(self) -> DeviceSkillRepository:
    method put (line 135) | def put(self, device_id: str):
    method _validate_request (line 150) | def _validate_request(self):
    method _get_skill (line 155) | def _get_skill(self):
    method _parse_skill_metadata (line 164) | def _parse_skill_metadata(self):
    method _ensure_settings_definition_exists (line 184) | def _ensure_settings_definition_exists(self):
    method _check_for_existing_settings_definition (line 191) | def _check_for_existing_settings_definition(self):
    method _add_settings_definition (line 202) | def _add_settings_definition(self):
    method _update_device_skill (line 210) | def _update_device_skill(self, device_id):
    method _get_device_skill (line 229) | def _get_device_skill(self, device_id):
    method _reconcile_skill_settings (line 244) | def _reconcile_skill_settings(self, settings_values):
    method _initialize_skill_settings (line 258) | def _initialize_skill_settings(self, device_id):

FILE: api/public/public_api/endpoints/device_skill_manifest.py
  class SkillManifestReconciler (line 39) | class SkillManifestReconciler(object):
    method __init__ (line 40) | def __init__(self, db, device_manifest, db_manifest):
    method reconcile (line 49) | def reconcile(self):
    method _update_skills (line 55) | def _update_skills(self):
    method _remove_skills (line 65) | def _remove_skills(self):
    method _add_skills (line 75) | def _add_skills(self):
  class RequestManifestSkill (line 86) | class RequestManifestSkill(Model):
  class SkillManifestRequest (line 98) | class SkillManifestRequest(Model):
  class DeviceSkillManifestEndpoint (line 107) | class DeviceSkillManifestEndpoint(PublicEndpoint):
    method __init__ (line 110) | def __init__(self):
    method device_skill_repo (line 114) | def device_skill_repo(self):
    method put (line 120) | def put(self, device_id):
    method _validate_put_request (line 127) | def _validate_put_request(self):
    method _update_skill_manifest (line 131) | def _update_skill_manifest(self, device_id):
    method _convert_manifest_timestamps (line 155) | def _convert_manifest_timestamps(manifest_skill):

FILE: api/public/public_api/endpoints/device_skill_settings.py
  function _normalize_field_value (line 51) | def _normalize_field_value(field):
  class SkillSettingUpdater (line 72) | class SkillSettingUpdater(object):
    method __init__ (line 83) | def __init__(self, db, device_id, display_data: dict):
    method device_skill_repo (line 91) | def device_skill_repo(self):
    method settings_display_repo (line 98) | def settings_display_repo(self):
    method update (line 104) | def update(self):
    method _extract_settings_values (line 110) | def _extract_settings_values(self):
    method _get_skill_id (line 134) | def _get_skill_id(self):
    method _ensure_settings_display_exists (line 143) | def _ensure_settings_display_exists(self) -> bool:
    method _upsert_device_skill (line 158) | def _upsert_device_skill(self):
    method _get_account_skill_settings (line 165) | def _get_account_skill_settings(self):
    method _update_skill_settings (line 175) | def _update_skill_settings(self, skill_settings):
    method _merge_settings_values (line 193) | def _merge_settings_values(self, settings_values=None):
    method _add_skill_to_device (line 210) | def _add_skill_to_device(self):
  class RequestSkillField (line 222) | class RequestSkillField(Model):
  class RequestSkillSection (line 233) | class RequestSkillSection(Model):
  class RequestSkillMetadata (line 238) | class RequestSkillMetadata(Model):
  class RequestSkillIcon (line 242) | class RequestSkillIcon(Model):
  class RequestSkill (line 247) | class RequestSkill(Model):
    method validate_skill_gid (line 257) | def validate_skill_gid(self, data, value):
  class DeviceSkillSettingsEndpoint (line 265) | class DeviceSkillSettingsEndpoint(PublicEndpoint):
    method device_skill_repo (line 274) | def device_skill_repo(self):
    method settings_display_repo (line 281) | def settings_display_repo(self):
    method skill_repo (line 288) | def skill_repo(self):
    method skill_setting_repo (line 295) | def skill_setting_repo(self):
    method get (line 301) | def get(self, device_id):
    method _build_response_data (line 325) | def _build_response_data(self, device_skills):
    method _apply_settings_values (line 351) | def _apply_settings_values(settings_definition, settings_values):
    method put (line 364) | def put(self, device_id):
    method _validate_put_request (line 372) | def _validate_put_request(self):
    method _update_skill_settings (line 376) | def _update_skill_settings(self, device_id):
    method _delete_orphaned_settings_display (line 387) | def _delete_orphaned_settings_display(self, settings_display_id):
  class DeviceSkillSettingsEndpointV2 (line 395) | class DeviceSkillSettingsEndpointV2(PublicEndpoint):
    method get (line 403) | def get(self, device_id):
    method _build_response_data (line 416) | def _build_response_data(self, device_id):
    method _build_response (line 426) | def _build_response(self, device_id, response_data):

FILE: api/public/public_api/endpoints/device_subscription.py
  class DeviceSubscriptionEndpoint (line 26) | class DeviceSubscriptionEndpoint(PublicEndpoint):
    method __init__ (line 27) | def __init__(self):
    method get (line 30) | def get(self, device_id):

FILE: api/public/public_api/endpoints/geolocation.py
  class GeolocationEndpoint (line 13) | class GeolocationEndpoint(PublicEndpoint):
    method __init__ (line 16) | def __init__(self):
    method city_repo (line 24) | def city_repo(self):
    method get (line 31) | def get(self):
    method _get_geolocation (line 38) | def _get_geolocation(self):
    method _get_cities (line 59) | def _get_cities(self):
    method _select_geolocation_from_cities (line 82) | def _select_geolocation_from_cities(self):
    method _get_city_for_requested_region (line 104) | def _get_city_for_requested_region(self):
    method _get_city_for_requested_country (line 120) | def _get_city_for_requested_country(self):

FILE: api/public/public_api/endpoints/google_stt.py
  class GoogleSTTEndpoint (line 50) | class GoogleSTTEndpoint(PublicEndpoint):
    method __init__ (line 53) | def __init__(self):
    method post (line 62) | def post(self):
    method _get_account (line 76) | def _get_account(self):
    method _check_for_open_dataset_agreement (line 81) | def _check_for_open_dataset_agreement(self):
    method _extract_audio_from_request (line 89) | def _extract_audio_from_request(self) -> AudioData:
    method _call_google_stt (line 111) | def _call_google_stt(self, audio: AudioData) -> str:
    method _add_transcription_metric (line 143) | def _add_transcription_metric(self):

FILE: api/public/public_api/endpoints/oauth_callback.py
  class OauthCallbackEndpoint (line 27) | class OauthCallbackEndpoint(PublicEndpoint):
    method __init__ (line 28) | def __init__(self):
    method get (line 32) | def get(self):

FILE: api/public/public_api/endpoints/open_weather_map.py
  class OpenWeatherMapEndpoint (line 28) | class OpenWeatherMapEndpoint(PublicEndpoint):
    method __init__ (line 31) | def __init__(self):
    method get (line 36) | def get(self, path):
    method _get_weather (line 41) | def _get_weather(self, path):

FILE: api/public/public_api/endpoints/premium_voice.py
  class PremiumVoiceEndpoint (line 27) | class PremiumVoiceEndpoint(PublicEndpoint):
    method __init__ (line 28) | def __init__(self):
    method get (line 31) | def get(self, device_id):
    method _get_premium_voice_link (line 42) | def _get_premium_voice_link(self, arch):

FILE: api/public/public_api/endpoints/stripe_webhook.py
  class StripeWebHookEndpoint (line 27) | class StripeWebHookEndpoint(PublicEndpoint):
    method __init__ (line 28) | def __init__(self):
    method post (line 31) | def post(self):

FILE: api/public/public_api/endpoints/wake_word_file.py
  class UploadRequest (line 49) | class UploadRequest(Model):
  class WakeWordFileUpload (line 58) | class WakeWordFileUpload(PublicEndpoint):
    method __init__ (line 70) | def __init__(self):
    method wake_word_repository (line 75) | def wake_word_repository(self):
    method wake_word (line 83) | def wake_word(self):
    method file_location (line 94) | def file_location(self):
    method post (line 108) | def post(self, device_id):
    method _validate_post_request (line 127) | def _validate_post_request(self):
    method _get_account (line 145) | def _get_account(self, device_id: str):
    method _save_audio_file (line 153) | def _save_audio_file(self, hashed_file_name: str, file_contents: bytes):
    method _add_wake_word_file (line 159) | def _add_wake_word_file(self, account: Account, hashed_file_name: str):

FILE: api/public/public_api/endpoints/wolfram_alpha.py
  class WolframAlphaEndpoint (line 29) | class WolframAlphaEndpoint(PublicEndpoint):
    method __init__ (line 38) | def __init__(self):
    method get (line 43) | def get(self):
    method _query_wolfram_alpha (line 48) | def _query_wolfram_alpha(self):

FILE: api/public/public_api/endpoints/wolfram_alpha_simple.py
  class WolframAlphaSimpleEndpoint (line 28) | class WolframAlphaSimpleEndpoint(PublicEndpoint):
    method __init__ (line 35) | def __init__(self):
    method get (line 40) | def get(self):

FILE: api/public/public_api/endpoints/wolfram_alpha_spoken.py
  class WolframAlphaSpokenEndpoint (line 28) | class WolframAlphaSpokenEndpoint(PublicEndpoint):
    method __init__ (line 31) | def __init__(self):
    method get (line 36) | def get(self):

FILE: api/public/public_api/endpoints/wolfram_alpha_v2.py
  class WolframAlphaV2Endpoint (line 28) | class WolframAlphaV2Endpoint(PublicEndpoint):
    method __init__ (line 34) | def __init__(self):
    method get (line 39) | def get(self):

FILE: api/public/tests/features/environment.py
  function public_api_client (line 60) | def public_api_client(context):
  function before_all (line 68) | def before_all(context):
  function after_all (line 85) | def after_all(context):
  function _remove_wake_word_files (line 106) | def _remove_wake_word_files(context, wake_word):
  function before_scenario (line 117) | def before_scenario(context, _):
  function after_scenario (line 129) | def after_scenario(context, _):
  function _add_account (line 146) | def _add_account(context):
  function _add_device (line 153) | def _add_device(context):
  function _add_skills (line 163) | def _add_skills(context):
  function _add_device_skills (line 183) | def _add_device_skills(context):
  function after_tag (line 199) | def after_tag(context, tag):
  function _delete_new_skill (line 207) | def _delete_new_skill(context):
  function _delete_stt_tagging_files (line 213) | def _delete_stt_tagging_files():
  function _delete_stt_transcription_metrics (line 220) | def _delete_stt_transcription_metrics(context):

FILE: api/public/tests/features/steps/common.py
  function check_device_last_contact (line 32) | def check_device_last_contact(context):
  function check_request_success (line 42) | def check_request_success(context):
  function check_request_success (line 49) | def check_request_success(context):
  function check_for_bad_request (line 54) | def check_for_bad_request(context, error_type):
  function build_request_header (line 68) | def build_request_header(context):
  function build_unauthorized_request_header (line 75) | def build_unauthorized_request_header(context):
  function validate_account_last_activity (line 82) | def validate_account_last_activity(context):
  function validate_account_activity_metrics (line 90) | def validate_account_activity_metrics(context):

FILE: api/public/tests/features/steps/device_email.py
  function send_email (line 28) | def send_email(context):
  function send_email_invalid_device (line 45) | def send_email_invalid_device(context):
  function _define_sendgrid_mock (line 50) | def _define_sendgrid_mock():
  function _call_email_endpoint (line 68) | def _call_email_endpoint(context, device_id=None):
  function validate_response (line 89) | def validate_response(context):

FILE: api/public/tests/features/steps/device_location.py
  function get_device_location (line 30) | def get_device_location(context):
  function validate_location (line 41) | def validate_location(context):
  function expire_location_etag (line 74) | def expire_location_etag(context):
  function get_using_expired_etag (line 84) | def get_using_expired_etag(context):
  function validate_etag (line 98) | def validate_etag(context):
  function valid_etag (line 106) | def valid_etag(context):
  function get_using_valid_etag (line 113) | def get_using_valid_etag(context):
  function validate_response_valid_etag (line 127) | def validate_response_valid_etag(context):

FILE: api/public/tests/features/steps/device_metrics.py
  function define_authorized_device (line 35) | def define_authorized_device(context, in_or_out):
  function define_unauthorized_device (line 40) | def define_unauthorized_device(context):
  function call_metrics_endpoint (line 45) | def call_metrics_endpoint(context):
  function validate_metric_in_db (line 62) | def validate_metric_in_db(context):

FILE: api/public/tests/features/steps/device_pairing.py
  function add_device (line 36) | def add_device(context):
  function get_device_pairing_code (line 55) | def get_device_pairing_code(context):
  function activate_device (line 65) | def activate_device(context):
  function set_pantacor_not_claimed (line 86) | def set_pantacor_not_claimed(context):
  function set_pantacor_not_claimed (line 91) | def set_pantacor_not_claimed(context):
  function activate_pantacor_device (line 96) | def activate_pantacor_device(context):
  function _mock_get_channel_response (line 119) | def _mock_get_channel_response() -> MagicMock:
  function _mock_get_device_response (line 135) | def _mock_get_device_response(context) -> MagicMock:
  function check_cached_pairing_data (line 159) | def check_cached_pairing_data(context):
  function validate_pairing_code_response (line 175) | def validate_pairing_code_response(context):
  function validate_activation_response (line 185) | def validate_activation_response(context):
  function validate_device_update (line 195) | def validate_device_update(context):
  function validate_pantacor_update (line 205) | def validate_pantacor_update(context):

FILE: api/public/tests/features/steps/device_refresh_token.py
  function refresh_token (line 28) | def refresh_token(context):
  function validate_refresh_token (line 38) | def validate_refresh_token(context):
  function refresh_invalid_token (line 55) | def refresh_invalid_token(context):
  function validate_refresh_invalid_token (line 63) | def validate_refresh_invalid_token(context):

FILE: api/public/tests/features/steps/device_skill_manifest.py
  function _build_manifest_upload (line 32) | def _build_manifest_upload(manifest_skills):
  function _add_device_specific_skill (line 52) | def _add_device_specific_skill(context):
  function upload_unchanged_skill_manifest (line 67) | def upload_unchanged_skill_manifest(context):
  function upload_changed_skill_manifest (line 74) | def upload_changed_skill_manifest(context):
  function upload_skill_manifest_with_deleted_skill (line 83) | def upload_skill_manifest_with_deleted_skill(context):
  function upload_skill_manifest_no_device_specific (line 90) | def upload_skill_manifest_no_device_specific(context):
  function upload_skill_manifest_with_new_skill (line 97) | def upload_skill_manifest_with_new_skill(context):
  function upload_malformed_skill_manifest (line 116) | def upload_malformed_skill_manifest(context):
  function _upload_skill_manifest (line 123) | def _upload_skill_manifest(context, skill_manifest):
  function get_unchanged_skill_manifest (line 134) | def get_unchanged_skill_manifest(context):
  function get_updated_skill_manifest (line 144) | def get_updated_skill_manifest(context):
  function get_empty_skill_manifest (line 156) | def get_empty_skill_manifest(context):
  function get_skill_manifest_no_device_specific (line 164) | def get_skill_manifest_no_device_specific(context):
  function ensure_device_specific_skill_removed (line 177) | def ensure_device_specific_skill_removed(context):
  function get_skill_manifest_new_skill (line 185) | def get_skill_manifest_new_skill(context):
  function get_new_skill (line 201) | def get_new_skill(context):

FILE: api/public/tests/features/steps/device_skill_settings.py
  function change_skill_setting_value (line 33) | def change_skill_setting_value(context):
  function delete_field_from_settings (line 41) | def delete_field_from_settings(context):
  function set_skill_setting_etag (line 49) | def set_skill_setting_etag(context):
  function expire_skill_setting_etag (line 56) | def expire_skill_setting_etag(context):
  function add_skill_not_assigned_to_device (line 64) | def add_skill_not_assigned_to_device(context):
  function get_device_skill_settings (line 77) | def get_device_skill_settings(context):
  function update_skill_settings (line 88) | def update_skill_settings(context, skill):
  function delete_skill (line 99) | def delete_skill(context):
  function validate_response (line 110) | def validate_response(context):
  function check_for_expired_etag (line 137) | def check_for_expired_etag(context):
  function _get_device_skill_settings (line 147) | def _get_device_skill_settings(context):
  function validate_updated_skill_setting_value (line 160) | def validate_updated_skill_setting_value(context):
  function validate_updated_skill_setting_value (line 170) | def validate_updated_skill_setting_value(context):
  function get_skills_etag (line 178) | def get_skills_etag(context):
  function validate_skill_setting_field_removed (line 188) | def validate_skill_setting_field_removed(context):

FILE: api/public/tests/features/steps/get_device.py
  function get_device (line 35) | def get_device(context):
  function validate_response (line 46) | def validate_response(context):
  function get_invalid_device (line 62) | def get_invalid_device(context):
  function get_not_allowed_device (line 69) | def get_not_allowed_device(context):
  function validate_invalid_response (line 78) | def validate_invalid_response(context):
  function update_device (line 84) | def update_device(context):
  function validate_update (line 99) | def validate_update(context):
  function get_device_etag (line 113) | def get_device_etag(context):
  function get_device_using_etag (line 120) | def get_device_using_etag(context):
  function validate_etag (line 135) | def validate_etag(context):
  function expire_etag (line 141) | def expire_etag(context):
  function fetch_device_expired_etag (line 149) | def fetch_device_expired_etag(context):
  function validate_status_code (line 164) | def validate_status_code(context):
  function validate_new_etag (line 170) | def validate_new_etag(context):

FILE: api/public/tests/features/steps/get_device_settings.py
  function get_device_settings (line 31) | def get_device_settings(context):
  function validate_response_setting (line 42) | def validate_response_setting(context):
  function get_device_settings (line 60) | def get_device_settings(context):
  function validate_response (line 69) | def validate_response(context):
  function get_device_setting_etag (line 75) | def get_device_setting_etag(context):
  function get_device_settings_using_etag (line 82) | def get_device_settings_using_etag(context):
  function validate_etag_response (line 96) | def validate_etag_response(context):
  function expire_etag_device_level (line 102) | def expire_etag_device_level(context):
  function expire_etag_account_level (line 110) | def expire_etag_account_level(context):
  function get_device_settings_using_etag (line 119) | def get_device_settings_using_etag(context):
  function validate_new_etag (line 135) | def validate_new_etag(context):

FILE: api/public/tests/features/steps/get_device_subscription.py
  function get_device_subscription (line 33) | def get_device_subscription(context):
  function validate_response (line 44) | def validate_response(context):
  function get_device_subscription (line 52) | def get_device_subscription(context):
  function validate_response_monthly (line 72) | def validate_response_monthly(context):
  function get_subscription_nonexistent_device (line 80) | def get_subscription_nonexistent_device(context):
  function validate_nonexistent_device (line 89) | def validate_nonexistent_device(context):

FILE: api/public/tests/features/steps/transcribe_audio.py
  function call_google_stt_endpoint (line 32) | def call_google_stt_endpoint(context, utterance):
  function call_audio_transcription_endpoint (line 44) | def call_audio_transcription_endpoint(context, utterance):
  function _build_audio_data (line 55) | def _build_audio_data() -> BytesIO:
  function _build_request_header (line 64) | def _build_request_header(context):
  function validate_google_response (line 73) | def validate_google_response(context):
  function validate_transcription_response (line 81) | def validate_transcription_response(context):
  function validate_transcription_metrics (line 87) | def validate_transcription_metrics(context):

FILE: api/public/tests/features/steps/wake_word_file.py
  function upload_known_wake_word_file (line 38) | def upload_known_wake_word_file(context):
  function add_collision_file (line 51) | def add_collision_file(context):
  function upload_unknown_wake_word_file (line 58) | def upload_unknown_wake_word_file(context):
  function _build_expected_wake_word_file (line 71) | def _build_expected_wake_word_file(context, wake_word):
  function _call_upload_endpoint (line 85) | def _call_upload_endpoint(context, metadata):
  function check_file_save (line 105) | def check_file_save(context):
  function check_wake_word_file_table (line 120) | def check_wake_word_file_table(context):

FILE: api/public/tests/features/steps/wolfram_alpha.py
  function send_question (line 27) | def send_question(context):
  function send_question (line 37) | def send_question(context):
  function send_question (line 47) | def send_question(context):
  function validate_response (line 57) | def validate_response(context):

FILE: api/sso/sso_api/api.py
  function add_cors_headers (line 113) | def add_cors_headers(response):

FILE: api/sso/sso_api/endpoints/authenticate_internal.py
  class AuthenticateInternalEndpoint (line 35) | class AuthenticateInternalEndpoint(SeleneEndpoint):
    method __init__ (line 38) | def __init__(self):
    method get (line 42) | def get(self):
    method _authenticate_credentials (line 50) | def _authenticate_credentials(self):

FILE: api/sso/sso_api/endpoints/github_token.py
  class GithubTokenEndpoint (line 26) | class GithubTokenEndpoint(SeleneEndpoint):
    method get (line 27) | def get(self):

FILE: api/sso/sso_api/endpoints/logout.py
  class LogoutEndpoint (line 30) | class LogoutEndpoint(SeleneEndpoint):
    method get (line 33) | def get(self):
    method _logout (line 39) | def _logout(self):

FILE: api/sso/sso_api/endpoints/password_change.py
  class PasswordChangeEndpoint (line 25) | class PasswordChangeEndpoint(CommonPasswordChangeEndpoint):
    method account_id (line 29) | def account_id(self):
    method _authenticate (line 33) | def _authenticate(self):

FILE: api/sso/sso_api/endpoints/password_reset.py
  class PasswordResetEndpoint (line 31) | class PasswordResetEndpoint(SeleneEndpoint):
    method post (line 34) | def post(self):
    method _get_account_from_email (line 45) | def _get_account_from_email(self):
    method _generate_reset_token (line 52) | def _generate_reset_token(self) -> str:
    method _send_reset_email (line 59) | def _send_reset_email(self, reset_token: str):
    method _send_account_not_found_email (line 75) | def _send_account_not_found_email(self):

FILE: api/sso/sso_api/endpoints/validate_federated.py
  class ValidateFederatedRequest (line 47) | class ValidateFederatedRequest(Model):
  class ValidateFederatedEndpoint (line 54) | class ValidateFederatedEndpoint(SeleneEndpoint):
    method __init__ (line 57) | def __init__(self):
    method post (line 61) | def post(self):
    method _validate_request (line 71) | def _validate_request(self):
    method _get_email_address (line 75) | def _get_email_address(self):
    method _get_account_by_email (line 83) | def _get_account_by_email(self):

FILE: api/sso/sso_api/endpoints/validate_token.py
  class ValidateTokenEndpoint (line 25) | class ValidateTokenEndpoint(SeleneEndpoint):
    method post (line 26) | def post(self):
    method _validate_token (line 30) | def _validate_token(self):

FILE: api/sso/tests/features/environment.py
  function sso_client (line 33) | def sso_client(context):
  function before_all (line 43) | def before_all(context):
  function before_scenario (line 51) | def before_scenario(context, _):
  function after_scenario (line 61) | def after_scenario(context, _):
  function after_all (line 72) | def after_all(context):

FILE: api/sso/tests/features/steps/add_account.py
  function build_new_account_request (line 32) | def build_new_account_request(context):
  function remove_required_field (line 47) | def remove_required_field(context, required_field):
  function reject_agreement (line 60) | def reject_agreement(context, agreement):
  function call_add_account_endpoint (line 69) | def call_add_account_endpoint(context):
  function call_validate_email_endpoint (line 81) | def call_validate_email_endpoint(context):
  function check_db_for_account (line 95) | def check_db_for_account(context):
  function check_db_for_account_metrics (line 112) | def check_db_for_account_metrics(context):
  function check_for_duplicate_account_error (line 128) | def check_for_duplicate_account_error(context):

FILE: api/sso/tests/features/steps/agreements.py
  function call_agreement_endpoint (line 29) | def call_agreement_endpoint(context, agreement):
  function validate_response (line 35) | def validate_response(context, agreement):

FILE: api/sso/tests/features/steps/common.py
  function check_request_success (line 27) | def check_request_success(context):
  function check_for_bad_request (line 33) | def check_for_bad_request(context, error_type):
  function check_error_message_exists (line 39) | def check_error_message_exists(context):
  function check_error_message (line 45) | def check_error_message(context, error_msg):

FILE: api/sso/tests/features/steps/login.py
  function save_credentials (line 34) | def save_credentials(context, email, password):
  function save_email (line 41) | def save_email(context, email, platform):
  function call_validate_federated_endpoint (line 48) | def call_validate_federated_endpoint(context):
  function call_internal_login_endpoint (line 62) | def call_internal_login_endpoint(context):
  function check_token_cookies (line 72) | def check_token_cookies(context):

FILE: api/sso/tests/features/steps/logout.py
  function use_account_with_valid_access_token (line 34) | def use_account_with_valid_access_token(context):
  function call_logout_endpoint (line 44) | def call_logout_endpoint(context):
  function check_response_cookies (line 52) | def check_response_cookies(context):

FILE: api/sso/tests/features/steps/password_change.py
  function setup_user (line 26) | def setup_user(context):
  function call_password_change_endpoint (line 37) | def call_password_change_endpoint(context):

FILE: batch/job_scheduler/jobs.py
  class JobRunner (line 38) | class JobRunner(object):
    method __init__ (line 41) | def __init__(self, script_name: str):
    method run_job (line 46) | def run_job(self):
    method _add_date_to_args (line 52) | def _add_date_to_args(self):
    method _build_command (line 63) | def _build_command(self):
    method _execute_command (line 74) | def _execute_command(self, command):
  function test_scheduler (line 92) | def test_scheduler():
  function load_skills (line 100) | def load_skills(version):
  function parse_core_metrics (line 108) | def parse_core_metrics():
  function partition_api_metrics (line 118) | def partition_api_metrics():
  function update_device_last_contact (line 129) | def update_device_last_contact():

FILE: batch/script/daily_report.py
  class DailyReport (line 42) | class DailyReport(SeleneScript):
    method __init__ (line 43) | def __init__(self):
    method _run (line 53) | def _run(self):
    method _build_report (line 62) | def _build_report(self, date: datetime = None):

FILE: batch/script/delete_wake_word_files.py
  class WakeWordFileRemover (line 33) | class WakeWordFileRemover(SeleneScript):
    method __init__ (line 38) | def __init__(self):
    method file_repository (line 42) | def file_repository(self):
    method _run (line 49) | def _run(self):
    method _delete_files_for_account (line 57) | def _delete_files_for_account(self, account_id, files_to_delete):
    method _remove_from_file_system (line 71) | def _remove_from_file_system(self, file_to_delete):
    method _remove_from_precise_file_system (line 80) | def _remove_from_precise_file_system(self, file_path):
    method _remove_from_local_file_system (line 91) | def _remove_from_local_file_system(self, file_path):
    method _check_for_empty_directory (line 104) | def _check_for_empty_directory(self, file_location):
    method _delete_empty_directory (line 124) | def _delete_empty_directory(self, file_location):
    method _run_on_precise_server (line 142) | def _run_on_precise_server(self, command):

FILE: batch/script/designate_wake_word_files.py
  class WakeWordFileDesignator (line 33) | class WakeWordFileDesignator(SeleneScript):
    method __init__ (line 38) | def __init__(self):
    method tag_names (line 47) | def tag_names(self):
    method _run (line 61) | def _run(self):
    method _get_designation_candidates (line 69) | def _get_designation_candidates(self) -> dict:
    method _assign_designations (line 77) | def _assign_designations(self, file_tags: List[FileTag]):
    method _init_tag (line 90) | def _init_tag(self, file_tag: FileTag):
    method _increment_value_counts (line 99) | def _increment_value_counts(self, tag_value_id: str):
    method _convert_tags_to_designations (line 108) | def _convert_tags_to_designations(self):
    method _apply_designation_criteria (line 115) | def _apply_designation_criteria(self) -> str:
    method _add_designation (line 129) | def _add_designation(self, designation_value):
    method _increment_designation_stats (line 141) | def _increment_designation_stats(self, designation_value):
    method _log_designation_stats (line 154) | def _log_designation_stats(self, wake_word):

FILE: batch/script/load_skill_display_data.py
  class SkillDisplayUpdater (line 42) | class SkillDisplayUpdater(SeleneScript):
    method __init__ (line 43) | def __init__(self):
    method _define_args (line 47) | def _define_args(self):
    method _run (line 56) | def _run(self):
    method _get_skill_display_data (line 64) | def _get_skill_display_data(self):
    method _update_skill_display_table (line 75) | def _update_skill_display_table(self):

FILE: batch/script/move_wake_word_files.py
  class WakeWordSampleMover (line 33) | class WakeWordSampleMover(SeleneScript):
    method __init__ (line 39) | def __init__(self):
    method _run (line 43) | def _run(self):
    method file_repository (line 48) | def file_repository(self):
    method _get_wake_word_file_info (line 54) | def _get_wake_word_file_info(self):
    method _move_files (line 60) | def _move_files(self, wake_word_file_info):
    method _ensure_remote_directory_exists (line 70) | def _ensure_remote_directory_exists(self, file_info) -> Path:
    method _ensure_directory_exists_on_server (line 79) | def _ensure_directory_exists_on_server(self, directory):
    method _ensure_directory_exists_on_db (line 86) | def _ensure_directory_exists_on_db(self, new_directory):
    method _copy_file (line 94) | def _copy_file(self, file_info, destination_dir):

FILE: batch/script/parse_core_metrics.py
  class CoreMetricsParser (line 38) | class CoreMetricsParser(SeleneScript):
    method __init__ (line 39) | def __init__(self):
    method _run (line 47) | def _run(self):
    method _start_new_interaction (line 57) | def _start_new_interaction(self, metric):
    method _add_metric_to_interaction (line 67) | def _add_metric_to_interaction(self, metric_value):
    method _add_interaction_to_db (line 99) | def _add_interaction_to_db(self):

FILE: batch/script/partition_api_metrics.py
  class PartitionApiMetrics (line 31) | class PartitionApiMetrics(SeleneScript):
    method __init__ (line 32) | def __init__(self):
    method _run (line 36) | def _run(self):

FILE: batch/script/test_scheduler.py
  class TestScheduler (line 31) | class TestScheduler(SeleneScript):
    method __init__ (line 32) | def __init__(self):
    method _define_args (line 35) | def _define_args(self):
    method _run (line 55) | def _run(self):

FILE: batch/script/update_device_last_contact.py
  class UpdateDeviceLastContact (line 36) | class UpdateDeviceLastContact(SeleneScript):
    method __init__ (line 37) | def __init__(self):
    method _run (line 41) | def _run(self):
    method _get_ts_from_cache (line 52) | def _get_ts_from_cache(self, device_id):

FILE: db/mycroft/account_schema/tables/account.sql
  type account (line 1) | CREATE TABLE account.account (

FILE: db/mycroft/account_schema/tables/account_agreement.sql
  type account (line 1) | CREATE TABLE account.account_agreement (

FILE: db/mycroft/account_schema/tables/account_membership.sql
  type account (line 1) | CREATE TABLE account.account_membership (

FILE: db/mycroft/account_schema/tables/agreement.sql
  type account (line 1) | CREATE TABLE account.agreement (

FILE: db/mycroft/account_schema/tables/membership.sql
  type account (line 1) | CREATE TABLE account.membership (

FILE: db/mycroft/device_schema/tables/account_defaults.sql
  type device (line 2) | CREATE TABLE device.account_defaults (

FILE: db/mycroft/device_schema/tables/account_preferences.sql
  type device (line 2) | CREATE TABLE device.account_preferences (

FILE: db/mycroft/device_schema/tables/category.sql
  type device (line 1) | CREATE TABLE device.category (

FILE: db/mycroft/device_schema/tables/device.sql
  type device (line 1) | CREATE TABLE device.device (

FILE: db/mycroft/device_schema/tables/device_skill.sql
  type device (line 1) | CREATE TABLE device.device_skill (

FILE: db/mycroft/device_schema/tables/geography.sql
  type device (line 1) | CREATE TABLE device.geography (

FILE: db/mycroft/device_schema/tables/pantacor_config.sql
  type device (line 1) | CREATE TABLE device.pantacor_config (

FILE: db/mycroft/device_schema/tables/skill_setting.sql
  type device (line 1) | CREATE TABLE device.skill_setting (

FILE: db/mycroft/device_schema/tables/text_to_speech.sql
  type device (line 1) | CREATE TABLE device.text_to_speech (

FILE: db/mycroft/device_schema/tables/wake_word.sql
  type device (line 1) | CREATE TABLE device.wake_word (

FILE: db/mycroft/device_schema/tables/wake_word_settings.sql
  type device (line 2) | CREATE TABLE device.wake_word_settings (

FILE: db/mycroft/geography_schema/tables/city.sql
  type geography (line 1) | CREATE TABLE geography.city (

FILE: db/mycroft/geography_schema/tables/country.sql
  type geography (line 1) | CREATE TABLE geography.country (

FILE: db/mycroft/geography_schema/tables/region.sql
  type geography (line 1) | CREATE TABLE geography.region (

FILE: db/mycroft/geography_schema/tables/timezone.sql
  type geography (line 1) | CREATE TABLE geography.timezone (

FILE: db/mycroft/metric_schema/tables/account_activity.sql
  type metric (line 1) | CREATE TABLE metric.account_activity (
  type account_activity_dt_idx (line 19) | CREATE INDEX IF NOT EXISTS

FILE: db/mycroft/metric_schema/tables/api.sql
  type metric (line 1) | CREATE TABLE metric.api (
  type api_access_ts_idx (line 15) | CREATE INDEX IF NOT EXISTS

FILE: db/mycroft/metric_schema/tables/api_history.sql
  type metric (line 1) | CREATE TABLE metric.api_history (

FILE: db/mycroft/metric_schema/tables/core.sql
  type metric (line 1) | CREATE TABLE metric.core (

FILE: db/mycroft/metric_schema/tables/core_interaction.sql
  type metric (line 1) | CREATE TABLE metric.core_interaction (

FILE: db/mycroft/metric_schema/tables/job.sql
  type metric (line 1) | CREATE TABLE metric.job (

FILE: db/mycroft/metric_schema/tables/stt_engine.sql
  type metric (line 1) | CREATE TABLE metric.stt_engine (
  type stt_transcription_engine_activity_idx (line 13) | CREATE UNIQUE INDEX IF NOT EXISTS

FILE: db/mycroft/metric_schema/tables/stt_transcription.sql
  type metric (line 5) | CREATE TABLE metric.stt_transcription (
  type stt_transcription_account_activity_idx (line 15) | CREATE UNIQUE INDEX IF NOT EXISTS

FILE: db/mycroft/skill_schema/tables/display.sql
  type skill (line 1) | CREATE TABLE skill.display (

FILE: db/mycroft/skill_schema/tables/oauth_credential.sql
  type skill (line 1) | CREATE TABLE skill.oauth_credential (

FILE: db/mycroft/skill_schema/tables/oauth_token.sql
  type skill (line 1) | CREATE TABLE skill.oauth_token (

FILE: db/mycroft/skill_schema/tables/settings_display.sql
  type skill (line 1) | CREATE TABLE skill.settings_display (

FILE: db/mycroft/skill_schema/tables/skill.sql
  type skill (line 1) | CREATE TABLE skill.skill (

FILE: db/mycroft/tagging_schema/tables/file_location.sql
  type tagging (line 2) | CREATE TABLE tagging.file_location (

FILE: db/mycroft/tagging_schema/tables/session.sql
  type tagging (line 2) | CREATE TABLE tagging.session (

FILE: db/mycroft/tagging_schema/tables/tag.sql
  type tagging (line 2) | CREATE TABLE tagging.tag (

FILE: db/mycroft/tagging_schema/tables/tag_value.sql
  type tagging (line 2) | CREATE TABLE tagging.tag_value (

FILE: db/mycroft/tagging_schema/tables/tagger.sql
  type tagging (line 2) | CREATE TABLE tagging.tagger (

FILE: db/mycroft/tagging_schema/tables/wake_word_file.sql
  type tagging (line 2) | CREATE TABLE tagging.wake_word_file (

FILE: db/mycroft/tagging_schema/tables/wake_word_file_designation.sql
  type tagging (line 2) | CREATE TABLE tagging.wake_word_file_designation (

FILE: db/mycroft/tagging_schema/tables/wake_word_file_tag.sql
  type tagging (line 2) | CREATE TABLE tagging.wake_word_file_tag (

FILE: db/mycroft/versions/2020.9.1.sql
  type wake_word (line 10) | CREATE TABLE wake_word.wake_word (
  type wake_word (line 17) | CREATE TABLE wake_word.pocketsphinx_settings (
  type tagging (line 145) | CREATE TABLE tagging.file_location (
  type tagging (line 152) | CREATE TABLE tagging.wake_word_file (
  type tagging (line 163) | CREATE TABLE tagging.tagger (
  type tagging (line 170) | CREATE TABLE tagging.session (
  type tagging (line 179) | CREATE TABLE tagging.tag (
  type tagging (line 186) | CREATE TABLE tagging.tag_value (
  type tagging (line 194) | CREATE TABLE tagging.wake_word_file_tag (
  type tagging (line 203) | CREATE TABLE tagging.wake_word_file_designation (

FILE: db/mycroft/wake_word_schema/tables/pocketsphinx_settings.sql
  type wake_word (line 2) | CREATE TABLE wake_word.pocketsphinx_settings (

FILE: db/mycroft/wake_word_schema/tables/wake_word.sql
  type wake_word (line 2) | CREATE TABLE wake_word.wake_word (

FILE: db/scripts/bootstrap_mycroft_db.py
  function get_sql_from_file (line 89) | def get_sql_from_file(file_path: str) -> str:
  class PostgresDB (line 96) | class PostgresDB(object):
    method __init__ (line 97) | def __init__(self, db_name, user=None):
    method close_db (line 122) | def close_db(self):
    method execute_sql (line 125) | def execute_sql(self, sql: str, args=None):
  function destroy_existing (line 131) | def destroy_existing(db):
  function create_anew (line 137) | def create_anew(db):
  function _init_db (line 143) | def _init_db():
  function _setup_template_db (line 150) | def _setup_template_db(db):
  function _build_schema_tables (line 162) | def _build_schema_tables(db, schema, tables):
  function _grant_access (line 169) | def _grant_access(db):
  function _build_template_db (line 175) | def _build_template_db():
  function _create_mycroft_db_from_template (line 189) | def _create_mycroft_db_from_template():
  function _apply_insert_file (line 196) | def _apply_insert_file(db, schema_dir, file_name):
  function _populate_agreement_table (line 204) | def _populate_agreement_table(db):
  function _populate_country_table (line 244) | def _populate_country_table(db):
  function _populate_region_table (line 264) | def _populate_region_table(db):
  function _populate_timezone_table (line 289) | def _populate_timezone_table(db):
  function _populate_city_table (line 316) | def _populate_city_table(db, continuous_integration):
  function _populate_db (line 368) | def _populate_db(continuous_integration):
  function _define_args (line 384) | def _define_args():

FILE: db/scripts/neo4j-postgres.py
  function load_csv (line 64) | def load_csv():
  function format_date (line 253) | def format_date(value):
  function format_timestamp (line 259) | def format_timestamp(value):
  function get_subscription_uuid (line 273) | def get_subscription_uuid(subs):
  function get_tts_uuid (line 289) | def get_tts_uuid(tts):
  function fill_account_table (line 302) | def fill_account_table():
  function fill_account_agreement_table (line 318) | def fill_account_agreement_table():
  function fill_default_wake_word (line 338) | def fill_default_wake_word():
  function fill_wake_word_table (line 375) | def fill_wake_word_table():
  function fill_account_preferences_table (line 409) | def fill_account_preferences_table():
  function fill_subscription_table (line 467) | def fill_subscription_table():
  function fill_wake_word_settings_table (line 506) | def fill_wake_word_settings_table():
  function change_device_name (line 552) | def change_device_name():
  function fill_device_table (line 569) | def fill_device_table():
  function fill_skills_table (line 717) | def fill_skills_table():
  function analyze_locations (line 774) | def analyze_locations():
  function analyze_location_2 (line 832) | def analyze_location_2():

FILE: db/scripts/remove_duplicate_cities.py
  function get_cursor (line 34) | def get_cursor():
  function get_duplicate_cities (line 50) | def get_duplicate_cities(cursor):
  function get_device_geographies (line 62) | def get_device_geographies(cursor, city):
  function get_account_defaults (line 80) | def get_account_defaults(cursor, city):
  function check_device_geography_for_dup_cities (line 95) | def check_device_geography_for_dup_cities(cursor, duplicate_cities):
  function check_account_defaults_for_dup_cities (line 113) | def check_account_defaults_for_dup_cities(cursor, duplicate_cities):
  function delete_duplicates (line 131) | def delete_duplicates(cursor, city, used_cities):
  function main (line 157) | def main():

FILE: shared/selene/api/base_config.py
  class APIConfigError (line 45) | class APIConfigError(Exception):
  class BaseConfig (line 49) | class BaseConfig(object):
  class DevelopmentConfig (line 67) | class DevelopmentConfig(BaseConfig):
  class TestConfig (line 72) | class TestConfig(BaseConfig):
  class ProdConfig (line 76) | class ProdConfig(BaseConfig):
  function get_base_config (line 80) | def get_base_config():

FILE: shared/selene/api/base_endpoint.py
  class APIError (line 37) | class APIError(Exception):
  class SeleneEndpoint (line 41) | class SeleneEndpoint(MethodView):
    method __init__ (line 50) | def __init__(self):
    method db (line 60) | def db(self):
    method _init_access_token (line 69) | def _init_access_token(self):
    method _init_refresh_token (line 73) | def _init_refresh_token(self):
    method _authenticate (line 76) | def _authenticate(self):
    method _validate_auth_tokens (line 92) | def _validate_auth_tokens(self):
    method _get_auth_tokens (line 109) | def _get_auth_tokens(self):
    method _decode_access_token (line 116) | def _decode_access_token(self):
    method _decode_refresh_token (line 123) | def _decode_refresh_token(self):
    method _get_account (line 133) | def _get_account(self, account_id):
    method _validate_account (line 138) | def _validate_account(self, account_id: str):
    method _refresh_auth_tokens (line 148) | def _refresh_auth_tokens(self):
    method _generate_tokens (line 153) | def _generate_tokens(self):
    method _set_token_cookies (line 160) | def _set_token_cookies(self, expire=False):

FILE: shared/selene/api/blueprint.py
  function handle_data_error (line 38) | def handle_data_error(error):
  function handle_data_error (line 43) | def handle_data_error(error):
  function handle_not_modified (line 48) | def handle_not_modified(_):
  function setup_request (line 53) | def setup_request():
  function teardown_request (line 58) | def teardown_request(response):
  function add_api_metric (line 65) | def add_api_metric(http_status):
  function update_device_last_contact (line 108) | def update_device_last_contact():

FILE: shared/selene/api/endpoints/account.py
  function agreement_accepted (line 67) | def agreement_accepted(value):
  class Login (line 73) | class Login(Model):
    method validate_email (line 81) | def validate_email(self, data, value):
    method validate_password (line 89) | def validate_password(self, data, value):
  class UpdateMembershipRequest (line 96) | class UpdateMembershipRequest(Model):
    method validate_membership_type (line 104) | def validate_membership_type(self, data, value):
    method validate_payment_method (line 109) | def validate_payment_method(self, data, value):
    method validate_payment_token (line 114) | def validate_payment_token(self, data, value):
  class AddAccountRequest (line 120) | class AddAccountRequest(Model):
  class AccountEndpoint (line 128) | class AccountEndpoint(SeleneEndpoint):
    method __init__ (line 134) | def __init__(self):
    method account_repository (line 140) | def account_repository(self):
    method account_activity_repository (line 148) | def account_activity_repository(self):
    method get (line 155) | def get(self):
    method _build_response_data (line 163) | def _build_response_data(self):
    method _format_agreement_date (line 179) | def _format_agreement_date(agreement):
    method _format_membership_duration (line 187) | def _format_membership_duration(response_data):
    method post (line 205) | def post(self):
    method _validate_post_request (line 213) | def _validate_post_request(self):
    method _build_login_schematic (line 226) | def _build_login_schematic(self) -> Login:
    method _determine_login_method (line 248) | def _determine_login_method(self):
    method _add_account (line 264) | def _add_account(self, email_address, password):
    method patch (line 281) | def patch(self):
    method _expire_device_setting_cache (line 295) | def _expire_device_setting_cache(self):
    method _update_account (line 300) | def _update_account(self):
    method _validate_membership_update_request (line 317) | def _validate_membership_update_request(value):
    method _update_membership (line 328) | def _update_membership(self, membership_change):
    method _get_active_membership (line 355) | def _get_active_membership(self):
    method _add_membership (line 364) | def _add_membership(self, membership_change, active_membership):
    method _get_stripe_plan (line 385) | def _get_stripe_plan(self, plan):
    method _cancel_membership (line 392) | def _cancel_membership(self, active_membership):
    method _update_username (line 399) | def _update_username(self, username):
    method _update_open_dataset_agreement (line 403) | def _update_open_dataset_agreement(self, opt_in: bool):
    method delete (line 413) | def delete(self):
    method _change_wake_word_file_status (line 424) | def _change_wake_word_file_status(self):

FILE: shared/selene/api/endpoints/agreements.py
  class AgreementsEndpoint (line 29) | class AgreementsEndpoint(SeleneEndpoint):
    method get (line 36) | def get(self, agreement_type):

FILE: shared/selene/api/endpoints/password_change.py
  class PasswordChangeEndpoint (line 28) | class PasswordChangeEndpoint(SeleneEndpoint):
    method account_id (line 32) | def account_id(self):
    method put (line 36) | def put(self):
    method _send_email (line 48) | def _send_email(self):

FILE: shared/selene/api/endpoints/validate_email.py
  class ValidateEmailEndpoint (line 35) | class ValidateEmailEndpoint(SeleneEndpoint):
    method get (line 38) | def get(self):
    method _get_email_address (line 52) | def _get_email_address(self):
    method _validate_email_address (line 65) | def _validate_email_address(self) -> str:

FILE: shared/selene/api/etag.py
  function device_etag_key (line 30) | def device_etag_key(device_id: str):
  function device_setting_etag_key (line 34) | def device_setting_etag_key(device_id: str):
  function device_location_etag_key (line 38) | def device_location_etag_key(device_id: str):
  class ETagManager (line 42) | class ETagManager(object):
    method __init__ (line 47) | def __init__(self, cache: SeleneCache, config: dict):
    method get (line 51) | def get(self, key: str) -> str:
    method expire (line 61) | def expire(self, key):
    method expire_device_etag_by_device_id (line 67) | def expire_device_etag_by_device_id(self, device_id: str):
    method expire_device_setting_etag_by_device_id (line 72) | def expire_device_setting_etag_by_device_id(self, device_id: str):
    method expire_device_setting_etag_by_account_id (line 77) | def expire_device_setting_etag_by_account_id(self, account_id: str):
    method expire_device_location_etag_by_device_id (line 85) | def expire_device_location_etag_by_device_id(self, device_id: str):
    method expire_device_location_etag_by_account_id (line 90) | def expire_device_location_etag_by_account_id(self, account_id: str):
    method expire_skill_etag_by_device_id (line 98) | def expire_skill_etag_by_device_id(self, device_id):
    method expire_skill_etag_by_account_id (line 105) | def expire_skill_etag_by_account_id(self, account_id):

FILE: shared/selene/api/pantacor.py
  class PantacorError (line 30) | class PantacorError(Exception):
  function _get_release_channels (line 34) | def _get_release_channels():
  function get_pantacor_device (line 44) | def get_pantacor_device(pantacor_device_id: str) -> PantacorConfig:
  function get_pantacor_pending_deployment (line 79) | def get_pantacor_pending_deployment(device_id: str):
  function apply_pantacor_update (line 101) | def apply_pantacor_update(deployment_id: str):
  function _change_pantacor_update_policy (line 110) | def _change_pantacor_update_policy(device_id: str, auto_update: bool):
  function _change_pantacor_release_channel (line 121) | def _change_pantacor_release_channel(device_id: str, release_channel: str):
  function _change_pantacor_ssh_key (line 143) | def _change_pantacor_ssh_key(device_id: str, ssh_key: str):
  function _call_pantacor_api (line 155) | def _call_pantacor_api(method: str, endpoint: str, **kwargs):
  function update_pantacor_config (line 187) | def update_pantacor_config(old_config: dict, new_config: dict):

FILE: shared/selene/api/public_endpoint.py
  function track_account_activity (line 39) | def track_account_activity(db, device_id: str):
  function check_oauth_token (line 48) | def check_oauth_token():
  function generate_device_login (line 76) | def generate_device_login(device_id: str, cache: SeleneCache) -> dict:
  function delete_device_login (line 99) | def delete_device_login(device_id: str, cache: SeleneCache):
  class PublicEndpoint (line 110) | class PublicEndpoint(MethodView):
    method __init__ (line 113) | def __init__(self):
    method db (line 124) | def db(self):
    method _authenticate (line 132) | def _authenticate(self, device_id: str = None):
    method _get_oauth_token_from_request (line 138) | def _get_oauth_token_from_request(self):
    method _get_device_id_from_token (line 147) | def _get_device_id_from_token(self, token):
    method _validate_request_device_id (line 156) | def _validate_request_device_id(self, request_device_id):
    method _add_etag (line 163) | def _add_etag(self, key):
    method _validate_etag (line 173) | def _validate_etag(self, key):

FILE: shared/selene/api/response.py
  function snake_to_camel (line 28) | def snake_to_camel(name):
  function coerce_response (line 33) | def coerce_response(response_data):
  class SeleneResponse (line 60) | class SeleneResponse(Response):
    method force_type (line 62) | def force_type(cls, rv, environ=None):

FILE: shared/selene/batch/base.py
  class SeleneScript (line 36) | class SeleneScript(object):
    method __init__ (line 40) | def __init__(self, job_file_path):
    method job_name (line 51) | def job_name(self):
    method db (line 59) | def db(self):
    method run (line 74) | def run(self):
    method _start_job (line 86) | def _start_job(self):
    method _define_args (line 93) | def _define_args(self):
    method _run (line 102) | def _run(self):
    method _finish_job (line 106) | def _finish_job(self):
    method _insert_metrics (line 115) | def _insert_metrics(self):

FILE: shared/selene/data/account/entity/account.py
  class AccountAgreement (line 27) | class AccountAgreement:
  class AccountMembership (line 36) | class AccountMembership:
  class Account (line 49) | class Account:

FILE: shared/selene/data/account/entity/agreement.py
  class Agreement (line 29) | class Agreement(object):

FILE: shared/selene/data/account/entity/membership.py
  class Membership (line 25) | class Membership(object):

FILE: shared/selene/data/account/entity/skill.py
  class AccountSkill (line 25) | class AccountSkill(object):

FILE: shared/selene/data/account/repository/account.py
  function _encrypt_password (line 35) | def _encrypt_password(password: str) -> str:
  class AccountRepository (line 48) | class AccountRepository(RepositoryBase):
    method __init__ (line 51) | def __init__(self, db):
    method add (line 56) | def add(self, account: Account, password: str) -> str:
    method _add_account (line 70) | def _add_account(self, account: Account, password: str):
    method add_agreement (line 93) | def add_agreement(self, account_id: str, agreement: AccountAgreement):
    method remove (line 105) | def remove(self, account: Account):
    method get_account_by_id (line 121) | def get_account_by_id(self, account_id: str) -> Optional[Account]:
    method get_account_by_email (line 136) | def get_account_by_email(self, email_address: str) -> Optional[Account]:
    method get_account_from_credentials (line 154) | def get_account_from_credentials(
    method get_account_by_device_id (line 176) | def get_account_by_device_id(self, device_id: str) -> Optional[Account]:
    method _get_account (line 186) | def _get_account(self, db_request: DatabaseRequest) -> Optional[Account]:
    method update_password (line 222) | def update_password(self, account_id: str, password: str):
    method update_email_address (line 235) | def update_email_address(self, account_id: str, email_address: str):
    method update_username (line 247) | def update_username(self, account_id: str, username: str):
    method expire_open_dataset_agreement (line 259) | def expire_open_dataset_agreement(self, account_id: str):
    method update_last_activity_ts (line 270) | def update_last_activity_ts(self, account_id: str):
    method daily_report (line 282) | def daily_report(self, date: datetime):
    method add_membership (line 407) | def add_membership(self, acct_id: str, membership: AccountMembership):
    method end_membership (line 425) | def end_membership(self, membership: AccountMembership):
    method end_active_membership (line 439) | def end_active_membership(self, customer_id: str):
    method get_active_account_membership (line 454) | def get_active_account_membership(self, account_id) -> Optional[Accoun...

FILE: shared/selene/data/account/repository/agreement.py
  class AgreementRepository (line 34) | class AgreementRepository(object):
    method __init__ (line 35) | def __init__(self, db):
    method add (line 41) | def add(self, agreement: Agreement) -> str:
    method _add_agreement_content (line 50) | def _add_agreement_content(self, content):
    method _add_agreement (line 60) | def _add_agreement(self, agreement: Agreement, content_id: int) -> str:
    method expire (line 80) | def expire(self, agreement: Agreement, expire_date: date):
    method remove (line 95) | def remove(self, agreement: Agreement):
    method _get_agreement_content_id (line 110) | def _get_agreement_content_id(self, agreement_id: str) -> int:
    method get_active (line 120) | def get_active(self):
    method get_active_for_type (line 142) | def get_active_for_type(self, agreement_type):
    method _get_agreement_content (line 150) | def _get_agreement_content(self, content_id):

FILE: shared/selene/data/account/repository/membership.py
  class MembershipRepository (line 28) | class MembershipRepository(RepositoryBase):
    method __init__ (line 29) | def __init__(self, db):
    method get_membership_types (line 32) | def get_membership_types(self):
    method get_membership_by_type (line 38) | def get_membership_by_type(self, membership_type: str):
    method add (line 45) | def add(self, membership: Membership):
    method remove (line 58) | def remove(self, membership: Membership):

FILE: shared/selene/data/account/repository/skill.py
  class AccountSkillRepository (line 26) | class AccountSkillRepository(RepositoryBase):
    method __init__ (line 27) | def __init__(self, db, account_id):
    method get_skills_for_account (line 31) | def get_skills_for_account(self) -> List[AccountSkill]:

FILE: shared/selene/data/device/entity/default.py
  class AccountDefaults (line 28) | class AccountDefaults:

FILE: shared/selene/data/device/entity/device.py
  class PantacorConfig (line 29) | class PantacorConfig:
  class Device (line 41) | class Device:

FILE: shared/selene/data/device/entity/device_skill.py
  class ManifestSkill (line 26) | class ManifestSkill(object):
  class AccountSkillSettings (line 39) | class AccountSkillSettings(object):
  class DeviceSkillSettings (line 48) | class DeviceSkillSettings(object):

FILE: shared/selene/data/device/entity/geography.py
  class Geography (line 25) | class Geography(object):

FILE: shared/selene/data/device/entity/preference.py
  class AccountPreferences (line 24) | class AccountPreferences(object):

FILE: shared/selene/data/device/entity/text_to_speech.py
  class TextToSpeech (line 24) | class TextToSpeech(object):

FILE: shared/selene/data/device/repository/default.py
  class DefaultsRepository (line 27) | class DefaultsRepository(RepositoryBase):
    method __init__ (line 30) | def __init__(self, db, account_id):
    method upsert (line 34) | def upsert(self, defaults):
    method get_account_defaults (line 44) | def get_account_defaults(self) -> AccountDefaults:

FILE: shared/selene/data/device/repository/device.py
  class DeviceRepository (line 30) | class DeviceRepository(RepositoryBase):
    method __init__ (line 33) | def __init__(self, db):
    method get_device_by_id (line 36) | def get_device_by_id(self, device_id: str) -> Device:
    method get_devices_by_account_id (line 54) | def get_devices_by_account_id(self, account_id: str) -> List[Device]:
    method _build_device_from_row (line 74) | def _build_device_from_row(row: dict) -> Device:
    method get_account_device_count (line 86) | def get_account_device_count(self, account_id: str) -> int:
    method get_all_device_ids (line 96) | def get_all_device_ids(self) -> List:
    method get_subscription_type_by_device_id (line 102) | def get_subscription_type_by_device_id(self, device_id: str):
    method add (line 122) | def add(self, account_id: str, device: dict) -> str:
    method update_device_from_core (line 133) | def update_device_from_core(self, device_id: str, updates: dict):
    method add_text_to_speech (line 142) | def add_text_to_speech(self, text_to_speech: TextToSpeech) -> str:
    method remove_wake_word (line 160) | def remove_wake_word(self, wake_word_id: str):
    method remove_text_to_speech (line 167) | def remove_text_to_speech(self, text_to_speech_id: str):
    method remove (line 175) | def remove(self, device_id: str):
    method update_device_from_account (line 186) | def update_device_from_account(
    method upsert_pantacor_config (line 203) | def upsert_pantacor_config(self, device_id: str, pantacor_config: Pant...
    method update_pantacor_config (line 225) | def update_pantacor_config(self, device_id: str, updates: dict):
    method update_last_contact_ts (line 239) | def update_last_contact_ts(self, device_id: str, last_contact_ts: date...

FILE: shared/selene/data/device/repository/device_skill.py
  class DeviceSkillRepository (line 34) | class DeviceSkillRepository(RepositoryBase):
    method __init__ (line 35) | def __init__(self, db):
    method get_skill_settings_for_account (line 38) | def get_skill_settings_for_account(
    method get_skill_settings_for_device (line 47) | def get_skill_settings_for_device(self, device_id, skill_id=None):
    method update_skill_settings (line 64) | def update_skill_settings(
    method upsert_device_skill_settings (line 75) | def upsert_device_skill_settings(
    method update_device_skill_settings (line 97) | def update_device_skill_settings(self, device_id, device_skill):
    method get_skill_manifest_for_device (line 114) | def get_skill_manifest_for_device(self, device_id: str) -> List[Manife...
    method get_skill_manifest_for_account (line 121) | def get_skill_manifest_for_account(self, account_id: str) -> List[Mani...
    method update_manifest_skill (line 128) | def update_manifest_skill(self, manifest_skill: ManifestSkill):
    method add_manifest_skill (line 135) | def add_manifest_skill(self, manifest_skill: ManifestSkill):
    method remove_manifest_skill (line 143) | def remove_manifest_skill(self, manifest_skill: ManifestSkill):
    method get_settings_display_usage (line 152) | def get_settings_display_usage(self, settings_display_id: str) -> int:
    method remove (line 161) | def remove(self, device_id, skill_id):

FILE: shared/selene/data/device/repository/geography.py
  class GeographyRepository (line 24) | class GeographyRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db, account_id):
    method get_account_geographies (line 29) | def get_account_geographies(self):
    method get_geography_id (line 38) | def get_geography_id(self, geography: Geography):
    method add (line 54) | def add(self, geography: Geography):
    method get_location_by_device_id (line 69) | def get_location_by_device_id(self, device_id):

FILE: shared/selene/data/device/repository/preference.py
  class PreferenceRepository (line 26) | class PreferenceRepository(RepositoryBase):
    method __init__ (line 27) | def __init__(self, db, account_id):
    method get_account_preferences (line 31) | def get_account_preferences(self) -> AccountPreferences:
    method upsert (line 45) | def upsert(self, preferences: AccountPreferences):

FILE: shared/selene/data/device/repository/setting.py
  class SettingRepository (line 28) | class SettingRepository:
    method __init__ (line 31) | def __init__(self, db):
    method get_device_settings_by_device_id (line 34) | def get_device_settings_by_device_id(self, device_id: str):
    method convert_text_to_speech_setting (line 44) | def convert_text_to_speech_setting(
    method _format_date_v1 (line 69) | def _format_date_v1(self, date: str) -> str:
    method _format_time_v1 (line 82) | def _format_time_v1(self, time: str) -> str:
    method get_device_settings (line 95) | def get_device_settings(self, device_id: str) -> Optional[dict]:
    method _get_open_dataset_agreement_by_device_id (line 126) | def _get_open_dataset_agreement_by_device_id(self, device_id: str) -> ...

FILE: shared/selene/data/device/repository/text_to_speech.py
  class TextToSpeechRepository (line 24) | class TextToSpeechRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_voices (line 28) | def get_voices(self):
    method add (line 34) | def add(self, text_to_speech: TextToSpeech):

FILE: shared/selene/data/geography/entity/city.py
  class City (line 24) | class City(object):
  class GeographicLocation (line 33) | class GeographicLocation(object):

FILE: shared/selene/data/geography/entity/country.py
  class Country (line 24) | class Country(object):

FILE: shared/selene/data/geography/entity/region.py
  class Region (line 24) | class Region(object):

FILE: shared/selene/data/geography/entity/timezone.py
  class Timezone (line 25) | class Timezone(object):

FILE: shared/selene/data/geography/repository/city.py
  class CityRepository (line 24) | class CityRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_cities_by_region (line 28) | def get_cities_by_region(self, region_id):
    method get_geographic_location_by_city (line 36) | def get_geographic_location_by_city(self, possible_city_names: list):
    method get_biggest_city_in_region (line 45) | def get_biggest_city_in_region(self, region_name):
    method get_biggest_city_in_country (line 53) | def get_biggest_city_in_country(self, country_name):

FILE: shared/selene/data/geography/repository/country.py
  class CountryRepository (line 24) | class CountryRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_countries (line 28) | def get_countries(self):

FILE: shared/selene/data/geography/repository/region.py
  class RegionRepository (line 24) | class RegionRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_regions_by_country (line 28) | def get_regions_by_country(self, country_id):

FILE: shared/selene/data/geography/repository/timezone.py
  class TimezoneRepository (line 24) | class TimezoneRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_timezones_by_country (line 28) | def get_timezones_by_country(self, country_id):

FILE: shared/selene/data/metric/entity/account_activity.py
  class AccountActivity (line 24) | class AccountActivity:

FILE: shared/selene/data/metric/entity/api.py
  class ApiMetric (line 26) | class ApiMetric(object):

FILE: shared/selene/data/metric/entity/core.py
  class CoreMetric (line 26) | class CoreMetric(object):
  class CoreInteraction (line 34) | class CoreInteraction(object):

FILE: shared/selene/data/metric/entity/job.py
  class JobMetric (line 25) | class JobMetric(object):

FILE: shared/selene/data/metric/entity/stt.py
  class SttTranscriptionMetric (line 27) | class SttTranscriptionMetric:
  class SttEngineMetric (line 38) | class SttEngineMetric:

FILE: shared/selene/data/metric/repository/account_activity.py
  class AccountActivityRepository (line 31) | class AccountActivityRepository(RepositoryBase):
    method __init__ (line 34) | def __init__(self, db):
    method increment_accounts_added (line 37) | def increment_accounts_added(self):
    method increment_accounts_deleted (line 42) | def increment_accounts_deleted(self):
    method increment_members_added (line 47) | def increment_members_added(self):
    method increment_members_expired (line 52) | def increment_members_expired(self):
    method increment_open_dataset_added (line 57) | def increment_open_dataset_added(self):
    method increment_open_dataset_deleted (line 64) | def increment_open_dataset_deleted(self):
    method increment_activity (line 71) | def increment_activity(self, account: Account):
    method _update_account_activity (line 90) | def _update_account_activity(self, update_request):
    method _add_account_activity_row (line 97) | def _add_account_activity_row(self):
    method get_activity_by_date (line 102) | def get_activity_by_date(self, activity_date: date) -> AccountActivity:
    method delete_activity_by_date (line 110) | def delete_activity_by_date(self, activity_date: date):

FILE: shared/selene/data/metric/repository/api.py
  class ApiMetricsRepository (line 40) | class ApiMetricsRepository(RepositoryBase):
    method __init__ (line 41) | def __init__(self, db):
    method add (line 44) | def add(self, metric: ApiMetric):
    method create_partition (line 50) | def create_partition(self, partition_date: date):
    method copy_to_partition (line 67) | def copy_to_partition(self, partition_date: date):
    method remove_by_date (line 80) | def remove_by_date(self, partition_date: date):

FILE: shared/selene/data/metric/repository/core.py
  class CoreMetricRepository (line 28) | class CoreMetricRepository(RepositoryBase):
    method __init__ (line 29) | def __init__(self, db):
    method add (line 32) | def add(self, metric: CoreMetric):
    method get_metrics_by_device (line 40) | def get_metrics_by_device(self, device_id):
    method get_metrics_by_date (line 47) | def get_metrics_by_date(self, metric_date: date) -> List[CoreMetric]:
    method add_interaction (line 54) | def add_interaction(self, interaction: CoreInteraction) -> str:

FILE: shared/selene/data/metric/repository/job.py
  class JobRepository (line 26) | class JobRepository(RepositoryBase):
    method __init__ (line 27) | def __init__(self, db):
    method add (line 30) | def add(self, job: JobMetric):

FILE: shared/selene/data/metric/repository/sql/create_api_metric_partition.sql
  type metric (line 1) | CREATE TABLE IF NOT EXISTS

FILE: shared/selene/data/metric/repository/sql/create_api_metric_partition_index.sql
  type _access_ts_idx (line 1) | CREATE INDEX IF NOT EXISTS

FILE: shared/selene/data/metric/repository/stt.py
  class TranscriptionMetricRepository (line 30) | class TranscriptionMetricRepository(RepositoryBase):
    method __init__ (line 33) | def __init__(self, db):
    method add (line 36) | def add(self, metric: SttTranscriptionMetric) -> str:
    method get_by_account (line 56) | def get_by_account(self, account_id: str) -> List[SttTranscriptionMetr...
    method delete_by_date (line 68) | def delete_by_date(self, transcription_date: date):

FILE: shared/selene/data/repository_base.py
  function _instantiate_dataclass (line 36) | def _instantiate_dataclass(dataclass, db_result):
  class RepositoryBase (line 51) | class RepositoryBase(object):
    method __init__ (line 52) | def __init__(self, db, repository_path):
    method _build_db_request (line 57) | def _build_db_request(
    method _build_db_batch_request (line 67) | def _build_db_batch_request(self, sql_file_name: str, args: List[dict]):
    method _select_one_into_dataclass (line 73) | def _select_one_into_dataclass(self, dataclass, sql_file_name, args=No...
    method _select_all_into_dataclass (line 84) | def _select_all_into_dataclass(self, dataclass, sql_file_name, args=No...

FILE: shared/selene/data/skill/entity/display.py
  class SkillDisplay (line 24) | class SkillDisplay(object):

FILE: shared/selene/data/skill/entity/skill.py
  class SkillVersion (line 25) | class SkillVersion(object):
  class Skill (line 31) | class Skill(object):
  class SkillFamily (line 37) | class SkillFamily(object):

FILE: shared/selene/data/skill/entity/skill_setting.py
  class AccountSkillSetting (line 25) | class AccountSkillSetting(object):
  class DeviceSkillSetting (line 32) | class DeviceSkillSetting(object):
  class SettingsDisplay (line 39) | class SettingsDisplay(object):

FILE: shared/selene/data/skill/repository/display.py
  class SkillDisplayRepository (line 24) | class SkillDisplayRepository(RepositoryBase):
    method __init__ (line 25) | def __init__(self, db):
    method get_display_data_for_skills (line 31) | def get_display_data_for_skills(self):
    method get_display_data_for_skill (line 38) | def get_display_data_for_skill(self, skill_display_id) -> SkillDisplay:
    method upsert (line 45) | def upsert(self, skill_display: SkillDisplay):

FILE: shared/selene/data/skill/repository/setting.py
  class SkillSettingRepository (line 29) | class SkillSettingRepository(RepositoryBase):
    method __init__ (line 30) | def __init__(self, db):
    method get_family_settings (line 34) | def get_family_settings(
    method get_installer_settings (line 43) | def get_installer_settings(self, account_id) -> List[AccountSkillSetti...
    method update_skill_settings (line 58) | def update_skill_settings(
    method get_skill_settings_for_device (line 76) | def get_skill_settings_for_device(self, device_id: str):

FILE: shared/selene/data/skill/repository/settings_display.py
  class SettingsDisplayRepository (line 26) | class SettingsDisplayRepository(RepositoryBase):
    method __init__ (line 27) | def __init__(self, db):
    method add (line 30) | def add(self, settings_display: SettingsDisplay) -> str:
    method get_settings_display_id (line 43) | def get_settings_display_id(self, settings_display: SettingsDisplay):
    method get_settings_definitions_by_gid (line 56) | def get_settings_definitions_by_gid(self, global_id):
    method remove (line 69) | def remove(self, settings_display_id: str):

FILE: shared/selene/data/skill/repository/skill.py
  function extract_family_from_global_id (line 26) | def extract_family_from_global_id(skill_gid: str) -> str:
  class SkillRepository (line 44) | class SkillRepository(RepositoryBase):
    method __init__ (line 47) | def __init__(self, db):
    method get_skills_for_account (line 51) | def get_skills_for_account(self, account_id: str) -> List[SkillFamily]:
    method get_skill_by_global_id (line 68) | def get_skill_by_global_id(self, skill_global_id: str) -> Skill:
    method ensure_skill_exists (line 80) | def ensure_skill_exists(self, skill_global_id: str) -> str:
    method _add_skill (line 95) | def _add_skill(self, skill_gid: str, name: str) -> str:
    method remove_by_gid (line 116) | def remove_by_gid(self, skill_gid):

FILE: shared/selene/data/tagging/entity/file_designation.py
  class FileDesignation (line 24) | class FileDesignation:

FILE: shared/selene/data/tagging/entity/file_location.py
  class TaggingFileLocation (line 24) | class TaggingFileLocation:

FILE: shared/selene/data/tagging/entity/file_tag.py
  class FileTag (line 24) | class FileTag:

FILE: shared/selene/data/tagging/entity/tag.py
  class Tag (line 27) | class Tag:

FILE: shared/selene/data/tagging/entity/tag_value.py
  class TagValue (line 24) | class TagValue:

FILE: shared/selene/data/tagging/entity/tagger.py
  class Tagger (line 24) | class Tagger:

FILE: shared/selene/data/tagging/entity/wake_word_file.py
  class WakeWordFile (line 29) | class WakeWordFile:
  class TaggableFile (line 43) | class TaggableFile:

FILE: shared/selene/data/tagging/repository/file_designation.py
  class FileDesignationRepository (line 27) | class FileDesignationRepository(RepositoryBase):
    method __init__ (line 30) | def __init__(self, db):
    method add (line 33) | def add(self, file_designation: FileDesignation):
    method get_from_date (line 41) | def get_from_date(self, wake_word, start_date) -> List[FileDesignation]:

FILE: shared/selene/data/tagging/repository/file_location.py
  class TaggingFileLocationRepository (line 24) | class TaggingFileLocationRepository(RepositoryBase):
    method __init__ (line 27) | def __init__(self, db):
    method ensure_location_exists (line 30) | def ensure_location_exists(
    method add (line 45) | def add(self, file_location: TaggingFileLocation) -> str:
    method get_id (line 59) | def get_id(self, file_location: TaggingFileLocation) -> str:
    method remove (line 73) | def remove(self, file_location: TaggingFileLocation):

FILE: shared/selene/data/tagging/repository/file_tag.py
  class FileTagRepository (line 27) | class FileTagRepository(RepositoryBase):
    method __init__ (line 30) | def __init__(self, db):
    method add (line 33) | def add(self, file_tag: FileTag):
    method get_designation_candidates (line 40) | def get_designation_candidates(self) -> defaultdict:

FILE: shared/selene/data/tagging/repository/session.py
  class SessionRepository (line 25) | class SessionRepository(RepositoryBase):
    method __init__ (line 28) | def __init__(self, db):
    method ensure_session_exists (line 31) | def ensure_session_exists(self, tagger: Tagger):
    method add (line 49) | def add(self, tagger: Tagger, note: str = None):
    method _get_active (line 63) | def _get_active(self, tagger: Tagger):
    method _end_session (line 84) | def _end_session(self, session_id, end_ts):

FILE: shared/selene/data/tagging/repository/tag.py
  class TagRepository (line 26) | class TagRepository(RepositoryBase):
    method __init__ (line 29) | def __init__(self, db):
    method get_all (line 32) | def get_all(self) -> List[Tag]:

FILE: shared/selene/data/tagging/repository/tagger.py
  class TaggerRepository (line 25) | class TaggerRepository(RepositoryBase):
    method __init__ (line 28) | def __init__(self, db):
    method ensure_tagger_exists (line 31) | def ensure_tagger_exists(self, tagger: Tagger) -> str:
    method _get_by_entity (line 43) | def _get_by_entity(self, tagger: Tagger):
    method _add (line 59) | def _add(self, tagger: Tagger):

FILE: shared/selene/data/tagging/repository/wake_word_file.py
  function build_tagging_file_name (line 41) | def build_tagging_file_name(file_contents):
  class WakeWordFileRepository (line 50) | class WakeWordFileRepository(RepositoryBase):
    method __init__ (line 53) | def __init__(self, db):
    method add (line 56) | def add(self, wake_word_file: WakeWordFile):
    method _handle_file_name_collision (line 102) | def _handle_file_name_collision(file_name: str, collisions: int):
    method get_by_wake_word (line 121) | def get_by_wake_word(self, wake_word: WakeWord) -> List[WakeWordFile]:
    method get_by_submission_date (line 140) | def get_by_submission_date(self, submission_date: date) -> List[WakeWo...
    method get_pending_delete (line 159) | def get_pending_delete(self) -> dict:
    method get_taggable_file (line 176) | def get_taggable_file(
    method _convert_db_row_to_dataclass (line 207) | def _convert_db_row_to_dataclass(row) -> WakeWordFile:
    method change_file_location (line 214) | def change_file_location(self, wake_word_file_id: str, file_location_i...
    method change_account_file_status (line 228) | def change_account_file_status(self, account_id: str, status: str):
    method change_file_status (line 240) | def change_file_status(self, wake_word_file: WakeWordFile, status: str):
    method remove (line 252) | def remove(self, wake_word_file: WakeWordFile):

FILE: shared/selene/data/wake_word/entity/pocketsphinx_settings.py
  class PocketsphinxSettings (line 24) | class PocketsphinxSettings(object):

FILE: shared/selene/data/wake_word/entity/wake_word.py
  class WakeWord (line 24) | class WakeWord(object):

FILE: shared/selene/data/wake_word/repository/wake_word.py
  class WakeWordRepository (line 28) | class WakeWordRepository(RepositoryBase):
    method __init__ (line 31) | def __init__(self, db):
    method get_wake_words_for_web (line 34) | def get_wake_words_for_web(self) -> List[WakeWord]:
    method ensure_wake_word_exists (line 46) | def ensure_wake_word_exists(self, name: str, engine: str) -> WakeWord:
    method get_id (line 60) | def get_id(self, wake_word: WakeWord) -> str:
    method add (line 74) | def add(self, wake_word: WakeWord) -> str:
    method remove (line 87) | def remove(self, wake_word: WakeWord):

FILE: shared/selene/testing/account.py
  function build_test_account (line 35) | def build_test_account(**overrides: Any):
  function add_account (line 53) | def add_account(db, **overrides: Any) -> Account:
  function remove_account (line 70) | def remove_account(db, account: Account):
  function build_test_membership (line 80) | def build_test_membership(**overrides: Any) -> AccountMembership:
  function add_account_membership (line 96) | def add_account_membership(db, account_id: str, **overrides: Any) -> Acc...

FILE: shared/selene/testing/account_activity.py
  function get_account_activity (line 27) | def get_account_activity(db):
  function remove_account_activity (line 32) | def remove_account_activity(db):
  function check_account_metrics (line 37) | def check_account_metrics(context, total, changed):

FILE: shared/selene/testing/account_geography.py
  function add_account_geography (line 23) | def add_account_geography(db, account, **overrides):

FILE: shared/selene/testing/account_preference.py
  function add_account_preference (line 23) | def add_account_preference(db, account_id):

FILE: shared/selene/testing/agreement.py
  function _build_test_terms_of_use (line 36) | def _build_test_terms_of_use():
  function _build_test_privacy_policy (line 48) | def _build_test_privacy_policy():
  function _build_open_dataset (line 65) | def _build_open_dataset():
  function add_agreements (line 73) | def add_agreements(context):
  function remove_agreements (line 87) | def remove_agreements(db, agreements: List[Agreement]):
  function get_agreements_from_api (line 93) | def get_agreements_from_api(context, agreement):
  function validate_agreement_response (line 105) | def validate_agreement_response(context, agreement):

FILE: shared/selene/testing/api.py
  function generate_access_token (line 33) | def generate_access_token(context, duration=ONE_MINUTE):
  function set_access_token_cookie (line 41) | def set_access_token_cookie(context, duration=ONE_MINUTE):
  function generate_refresh_token (line 50) | def generate_refresh_token(context, duration=TWO_MINUTES):
  function set_refresh_token_cookie (line 60) | def set_refresh_token_cookie(context, duration=TWO_MINUTES):
  function validate_token_cookies (line 69) | def validate_token_cookies(context, expired=False):
  function _parse_cookie (line 89) | def _parse_cookie(cookie: str) -> dict:
  function get_account (line 101) | def get_account(context) -> Account:
  function check_http_success (line 109) | def check_http_success(context):
  function check_http_error (line 115) | def check_http_error(context, error_type):

FILE: shared/selene/testing/device.py
  function add_device (line 23) | def add_device(db, account_id, geography_id):
  function add_pantacor_config (line 43) | def add_pantacor_config(db, device_id):

FILE: shared/selene/testing/device_skill.py
  function add_device_skill (line 26) | def add_device_skill(db, device_id, skill):
  function add_device_skill_settings (line 42) | def add_device_skill_settings(db, device_id, settings_display, settings_...
  function remove_device_skill (line 49) | def remove_device_skill(db, manifest_skill):

FILE: shared/selene/testing/membership.py
  function insert_memberships (line 38) | def insert_memberships(db):
  function delete_memberships (line 48) | def delete_memberships(db, memberships):

FILE: shared/selene/testing/skill.py
  function build_text_field (line 28) | def build_text_field():
  function build_checkbox_field (line 37) | def build_checkbox_field():
  function build_label_field (line 41) | def build_label_field():
  function _build_display_data (line 45) | def _build_display_data(skill_gid, fields):
  function add_skill (line 64) | def add_skill(db, skill_global_id, settings_fields=None):
  function remove_skill (line 76) | def remove_skill(db, skill):

FILE: shared/selene/testing/tagging.py
  function remove_wake_word_files (line 29) | def remove_wake_word_files(db, wake_word_file):
  function add_wake_word_file (line 35) | def add_wake_word_file(context, file_name):

FILE: shared/selene/testing/test_db.py
  function create_test_db (line 27) | def create_test_db():
  function drop_test_db (line 41) | def drop_test_db():

FILE: shared/selene/testing/text_to_speech.py
  function _build_voice (line 23) | def _build_voice():
  function add_text_to_speech (line 31) | def add_text_to_speech(db):
  function remove_text_to_speech (line 39) | def remove_text_to_speech(db, voice):

FILE: shared/selene/testing/wake_word.py
  function add_wake_word (line 24) | def add_wake_word(db) -> WakeWord:
  function remove_wake_word (line 37) | def remove_wake_word(db, wake_word: WakeWord):

FILE: shared/selene/util/auth.py
  class AuthenticationError (line 35) | class AuthenticationError(Exception):
  class AuthenticationToken (line 39) | class AuthenticationToken:
    method __init__ (line 43) | def __init__(self, secret: str, duration: int):
    method generate (line 51) | def generate(self, account_id: str):
    method validate (line 62) | def validate(self):
  function get_google_account_email (line 81) | def get_google_account_email(token: str) -> str:
  function get_facebook_account_email (line 99) | def get_facebook_account_email(token: str) -> str:
  function get_github_account_email (line 111) | def get_github_account_email(token: str) -> str:
  function get_github_authentication_token (line 131) | def get_github_authentication_token(access_code: str, state: str) -> str:

FILE: shared/selene/util/cache.py
  class SeleneCache (line 31) | class SeleneCache(object):
    method __init__ (line 32) | def __init__(self):
    method set_if_not_exists_with_expiration (line 38) | def set_if_not_exists_with_expiration(
    method set_with_expiration (line 51) | def set_with_expiration(self, key, value, expiration: int):
    method get (line 56) | def get(self, key):
    method delete (line 60) | def delete(self, key):
    method set (line 64) | def set(self, key, value):

FILE: shared/selene/util/db/connection.py
  class DBConnectionError (line 37) | class DBConnectionError(Exception):
  class DatabaseConnectionConfig (line 42) | class DatabaseConnectionConfig:
    method __post_init__ (line 55) | def __post_init__(self, use_namedtuple_cursor: bool):
  function connect_to_db (line 60) | def connect_to_db(connection_config: DatabaseConnectionConfig):

FILE: shared/selene/util/db/connection_pool.py
  function allocate_db_connection_pool (line 33) | def allocate_db_connection_pool(
  function get_db_connection (line 71) | def get_db_connection(connection_pool, autocommit=True):
  function get_db_connection_from_pool (line 90) | def get_db_connection_from_pool(connection_pool, autocommit=True):
  function return_db_connection_to_pool (line 103) | def return_db_connection_to_pool(connection_pool, connection):

FILE: shared/selene/util/db/cursor.py
  function get_sql_from_file (line 36) | def get_sql_from_file(file_path: str) -> str:
  class DatabaseRequest (line 53) | class DatabaseRequest:
  class DatabaseBatchRequest (line 61) | class DatabaseBatchRequest:
  class Cursor (line 68) | class Cursor:
    method __init__ (line 71) | def __init__(self, db):
    method _fetch (line 74) | def _fetch(self, db_request: DatabaseRequest, singleton=False):
    method select_one (line 93) | def select_one(self, db_request: DatabaseRequest):
    method select_all (line 102) | def select_all(self, db_request: DatabaseRequest):
    method execute (line 111) | def execute(self, db_request: DatabaseRequest):
    method _execute_batch (line 124) | def _execute_batch(self, db_request: DatabaseBatchRequest):
    method delete (line 130) | def delete(self, db_request: DatabaseRequest):
    method insert (line 135) | def insert(self, db_request: DatabaseRequest):
    method insert_returning (line 139) | def insert_returning(self, db_request: DatabaseRequest):
    method update (line 143) | def update(self, db_request: DatabaseRequest):
    method batch_update (line 148) | def batch_update(self, db_request: DatabaseBatchRequest):
    method dump_query_result_to_file (line 152) | def dump_query_result_to_file(
    method load_dump_file_to_table (line 166) | def load_dump_file_to_table(self, table_name: str, dump_file_path: str):

FILE: shared/selene/util/db/transaction.py
  function use_transaction (line 24) | def use_transaction(func):

FILE: shared/selene/util/email/email.py
  function validate_email_address (line 35) | def validate_email_address(email_address: str) -> Tuple[Optional[str], O...
  class EmailMessage (line 53) | class EmailMessage:
    method __post_init__ (line 64) | def __post_init__(self):
  class SeleneMailer (line 71) | class SeleneMailer:  # pylint: disable=too-few-public-methods
    method __init__ (line 76) | def __init__(self, message: EmailMessage):
    method send (line 80) | def send(self, using_jinja: bool = False):
    method _build_content (line 100) | def _build_content(self, using_jinja: bool) -> Content:
    method _build_content_from_html_template (line 116) | def _build_content_from_html_template(self):
    method _build_content_from_jinja_template (line 128) | def _build_content_from_jinja_template(self):

FILE: shared/selene/util/exceptions.py
  class NotModifiedException (line 23) | class NotModifiedException(Exception):

FILE: shared/selene/util/github.py
  function log_into_github (line 31) | def log_into_github(user_name: str, user_password: str) -> Github:
  function download_repository_file (line 37) | def download_repository_file(

FILE: shared/selene/util/log.py
  function _generate_log_config (line 47) | def _generate_log_config(service: str) -> dict:
  function configure_selene_logger (line 81) | def configure_selene_logger(service):
  function get_selene_logger (line 97) | def get_selene_logger(module_name: str):

FILE: shared/selene/util/payment/stripe.py
  function create_stripe_account (line 25) | def create_stripe_account(token: str, email: str):
  function create_stripe_subscription (line 31) | def create_stripe_subscription(customer_id, plan):
  function cancel_stripe_subscription (line 38) | def cancel_stripe_subscription(subscription_id):

FILE: shared/selene/util/ssh/sftp.py
  function get_remote_file (line 28) | def get_remote_file(ssh_config: SshClientConfig, local_path: Path, remot...

FILE: shared/selene/util/ssh/ssh.py
  class SshClientConfig (line 37) | class SshClientConfig:
    method __post_init__ (line 47) | def __post_init__(self):
  class SeleneSshClient (line 55) | class SeleneSshClient:
    method __init__ (line 60) | def __init__(self, config: SshClientConfig):
    method _check_ssh_key (line 67) | def _check_ssh_key(self):
    method client (line 78) | def client(self):
    method connect (line 87) | def connect(self):
    method disconnect (line 106) | def disconnect(self):
  function validate_rsa_public_key (line 111) | def validate_rsa_public_key(public_key: str) -> bool:
  function _parse_public_key (line 137) | def _parse_public_key(public_key: str) -> Tuple[str, str]:
Condensed preview — 538 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,107K chars).
[
  {
    "path": ".editorconfig",
    "chars": 347,
    "preview": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\nin"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 4454,
    "preview": "# How to contribute\n\nSo you want to contribute to Mycroft?\nThis should be as easy as possible for you but there are a fe"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 2086,
    "preview": "# How to submit an Issue to a Mycroft repository\n\nWhen submitting an Issue to a Mycroft repository, please follow these "
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 288,
    "preview": "## Description\n(Description of what the PR does, such as fixes # {issue number})\n\n## How to test\n(Description of how to "
  },
  {
    "path": ".github/SUPPORT.md",
    "chars": 999,
    "preview": "# How to get support with Mycroft software, hardware and products\n\nThere are multiple ways to seek support with Mycroft "
  },
  {
    "path": ".gitignore",
    "chars": 564,
    "preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/tmp\n/out-tsc\n**/*.eg"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 260,
    "preview": "repos:\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v2.3.0\n    hooks:\n    -   id: check-yaml\n    - "
  },
  {
    "path": "AUTHORS",
    "chars": 215,
    "preview": "The Mycroft Server was initially developed by Mycroft AI Inc\n\nIt lives on as an open source project with many contributo"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3218,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "Dockerfile",
    "chars": 5449,
    "preview": "# Multi-stage Dockerfile for running Selene APIs or their test suites.\n#\n# ASSUMPTION:\n#   This Dockerfile assumes its r"
  },
  {
    "path": "Jenkinsfile",
    "chars": 10932,
    "preview": "pipeline {\n    agent any\n    options {\n        // Running builds concurrently could cause a race condition with\n        "
  },
  {
    "path": "LICENSE",
    "chars": 34522,
    "preview": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C)"
  },
  {
    "path": "README.md",
    "chars": 20154,
    "preview": "[![License](https://img.shields.io/badge/License-GNU_AGPL%203.0-blue.svg)](LICENSE)\n[![CLA](https://img.shields.io/badge"
  },
  {
    "path": "api/account/account_api/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/api.py",
    "chars": 6277,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/__init__.py",
    "chars": 1807,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/change_email_address.py",
    "chars": 3650,
    "preview": "#  Mycroft Server - Backend\n#  Copyright (c) 2022 Mycroft AI Inc\n#  SPDX-License-Identifier: \tAGPL-3.0-or-later\n#  #\n#  "
  },
  {
    "path": "api/account/account_api/endpoints/change_password.py",
    "chars": 1601,
    "preview": "#  Mycroft Server - Backend\n#  Copyright (c) 2022 Mycroft AI Inc\n#  SPDX-License-Identifier: \tAGPL-3.0-or-later\n#  #\n#  "
  },
  {
    "path": "api/account/account_api/endpoints/city.py",
    "chars": 1484,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/country.py",
    "chars": 1148,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/defaults.py",
    "chars": 3743,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/device.py",
    "chars": 17032,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/device_count.py",
    "chars": 1324,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/geography.py",
    "chars": 1531,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/membership.py",
    "chars": 1294,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/pairing_code.py",
    "chars": 1646,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/preferences.py",
    "chars": 3265,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/region.py",
    "chars": 1208,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/skill_oauth.py",
    "chars": 1470,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/skill_settings.py",
    "chars": 5125,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/skills.py",
    "chars": 2066,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/software_update.py",
    "chars": 1841,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2021 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/ssh_key_validator.py",
    "chars": 1557,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2021 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/timezone.py",
    "chars": 1382,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/verify_email_address.py",
    "chars": 2696,
    "preview": "#  Mycroft Server - Backend\n#  Copyright (c) 2022 Mycroft AI Inc\n#  SPDX-License-Identifier: \tAGPL-3.0-or-later\n#  #\n#  "
  },
  {
    "path": "api/account/account_api/endpoints/voice_endpoint.py",
    "chars": 1327,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/account_api/endpoints/wake_word_endpoint.py",
    "chars": 1724,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/pyproject.toml",
    "chars": 988,
    "preview": "[tool.poetry]\nname = \"account\"\nversion = \"0.1.0\"\ndescription = \"API to support account.mycroft.ai\"\nauthors = [\"Chris Vei"
  },
  {
    "path": "api/account/tests/features/add_device.feature",
    "chars": 406,
    "preview": "Feature: Account API -- Pair a device\n  Test the device add endpoint\n\n  Scenario: Add a device\n    Given an account\n    "
  },
  {
    "path": "api/account/tests/features/agreements.feature",
    "chars": 488,
    "preview": "Feature: Account API -- Get the active agreements\n  We need to be able to retrieve an agreement and display it on the we"
  },
  {
    "path": "api/account/tests/features/authentication.feature",
    "chars": 1431,
    "preview": "Feature: Account API - Authentication with JWTs\n  Some of the API endpoints contain information that is specific to a us"
  },
  {
    "path": "api/account/tests/features/environment.py",
    "chars": 3834,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/pantacor_update.feature",
    "chars": 1511,
    "preview": "Feature: Account API -- Interact with the Pantacor API\n  Devices that use Pantacor to manage the software running on the"
  },
  {
    "path": "api/account/tests/features/profile.feature",
    "chars": 2993,
    "preview": "Feature: Account API -- Manage account profiles\n  Test the ability of the account API to retrieve and manage a user's pr"
  },
  {
    "path": "api/account/tests/features/remove_account.feature",
    "chars": 1023,
    "preview": "Feature: Account API -- Delete an account\n  Test the API call to delete an account and all its related data from the dat"
  },
  {
    "path": "api/account/tests/features/steps/add_device.py",
    "chars": 4006,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/agreements.py",
    "chars": 1876,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/authentication.py",
    "chars": 3439,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/common.py",
    "chars": 2042,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/pantacor_update.py",
    "chars": 4903,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/profile.py",
    "chars": 14869,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/tests/features/steps/remove_account.py",
    "chars": 4370,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/account/uwsgi.ini",
    "chars": 129,
    "preview": "[uwsgi]\nmaster = true\nmodule = account_api.api:acct\nprocesses = 4\nsocket = :5000\ndie-on-term = true\nlazy = true\nlazy-app"
  },
  {
    "path": "api/market/market_api/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/api.py",
    "chars": 2399,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/endpoints/__init__.py",
    "chars": 1026,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/endpoints/available_skills.py",
    "chars": 5308,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/endpoints/skill_detail.py",
    "chars": 3848,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/endpoints/skill_install.py",
    "chars": 4556,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/market_api/endpoints/skill_install_status.py",
    "chars": 5416,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/market/pyproject.toml",
    "chars": 521,
    "preview": "[tool.poetry]\nname = \"market\"\nversion = \"0.1.0\"\ndescription = \"API for Mycroft Marketplace\"\nauthors = [\"Chris Veilleux <"
  },
  {
    "path": "api/market/swagger.yaml",
    "chars": 7171,
    "preview": "swagger: '2.0'\ninfo:\n  description: >-\n    The marketplace is where users can access the skills available to install on\n"
  },
  {
    "path": "api/market/uwsgi.ini",
    "chars": 130,
    "preview": "[uwsgi]\nmaster = true\nmodule = market_api.api:market\nprocesses = 4\nsocket = :5000\ndie-on-term = true\nlazy = true\nlazy-ap"
  },
  {
    "path": "api/precise/precise_api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "api/precise/precise_api/api.py",
    "chars": 1808,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/precise/precise_api/endpoints/__init__.py",
    "chars": 989,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/precise/precise_api/endpoints/audio_file.py",
    "chars": 1380,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/precise/precise_api/endpoints/designation.py",
    "chars": 5319,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/precise/precise_api/endpoints/tag.py",
    "chars": 8144,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/precise/pyproject.toml",
    "chars": 477,
    "preview": "[tool.poetry]\nname = \"precise\"\nversion = \"0.1.0\"\ndescription = \"API for Precise wake word tagger\"\nauthors = [\"Chris Veil"
  },
  {
    "path": "api/precise/uwsgi.ini",
    "chars": 132,
    "preview": "[uwsgi]\nmaster = true\nmodule = precise_api.api:precise\nprocesses = 4\nsocket = :5000\ndie-on-term = true\nlazy = true\nlazy-"
  },
  {
    "path": "api/public/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/api.py",
    "chars": 7811,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/audio_transcription.py",
    "chars": 5314,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device.py",
    "chars": 2759,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_activate.py",
    "chars": 4871,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_code.py",
    "chars": 4618,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_email.py",
    "chars": 2309,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_location.py",
    "chars": 1584,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_metrics.py",
    "chars": 1644,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_oauth.py",
    "chars": 1580,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_pantacor.py",
    "chars": 3900,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2022 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_refresh_token.py",
    "chars": 3050,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_setting.py",
    "chars": 1585,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_skill.py",
    "chars": 11293,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_skill_manifest.py",
    "chars": 5739,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_skill_settings.py",
    "chars": 16281,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/device_subscription.py",
    "chars": 1523,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/geolocation.py",
    "chars": 5091,
    "preview": "\"\"\"Call this endpoint to retrieve the timezone for a given location\"\"\"\nfrom http import HTTPStatus\n\nfrom selene.api impo"
  },
  {
    "path": "api/public/public_api/endpoints/google_stt.py",
    "chars": 6100,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/oauth_callback.py",
    "chars": 1292,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/open_weather_map.py",
    "chars": 1581,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/premium_voice.py",
    "chars": 1771,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/stripe_webhook.py",
    "chars": 1386,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/wake_word_file.py",
    "chars": 6732,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/wolfram_alpha.py",
    "chars": 2012,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/wolfram_alpha_simple.py",
    "chars": 1733,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2021 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/wolfram_alpha_spoken.py",
    "chars": 1612,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/public_api/endpoints/wolfram_alpha_v2.py",
    "chars": 1698,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/pyproject.toml",
    "chars": 1116,
    "preview": "[tool.poetry]\nname = \"public\"\nversion = \"0.1.0\"\ndescription = \"API for interactions between Selene and Mycroft devices\"\n"
  },
  {
    "path": "api/public/tests/features/device_email.feature",
    "chars": 648,
    "preview": "Feature: Device API -- Send email to the account holder\n  Some skills have the ability to send email upon request.  One "
  },
  {
    "path": "api/public/tests/features/device_location.feature",
    "chars": 817,
    "preview": "Feature: Device API -- Request device location\n\n  Scenario: Location is successfully retrieved from a device\n    When a "
  },
  {
    "path": "api/public/tests/features/device_metrics.feature",
    "chars": 695,
    "preview": "Feature: Device API -- Save device activity metrics\n\n  Scenario: User opted into the open dataset uses their device\n    "
  },
  {
    "path": "api/public/tests/features/device_pairing.feature",
    "chars": 1088,
    "preview": "Feature: Device API -- Pair a device\n  Test the device pairing workflow\n\n  Scenario: Pairing code generation\n    When a "
  },
  {
    "path": "api/public/tests/features/device_refresh_token.feature",
    "chars": 589,
    "preview": "#Feature: Refresh device token\n#  Test the endpoint used to refresh the device session login\n#\n#  Scenario: A valid logi"
  },
  {
    "path": "api/public/tests/features/device_skill_manifest.feature",
    "chars": 2187,
    "preview": "Feature: Device API -- Upload and fetch skills manifest\n\n  Scenario: Device uploads an unchanged manifest\n    Given an a"
  },
  {
    "path": "api/public/tests/features/device_skill_settings.feature",
    "chars": 2612,
    "preview": "Feature: Device API -- Upload and fetch skills and their settings\n  Test all endpoints related to upload and fetch skill"
  },
  {
    "path": "api/public/tests/features/device_subscription.feature",
    "chars": 749,
    "preview": "Feature: Device API -- Request account subscription type\n  Test the endpoint used to fetch the subscription type of a de"
  },
  {
    "path": "api/public/tests/features/environment.py",
    "chars": 8139,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/get_device.feature",
    "chars": 1335,
    "preview": "Feature: Device API - Request device's information\n  Test the endpoint to get a device\n\n  Scenario: A valid device entit"
  },
  {
    "path": "api/public/tests/features/get_device_settings.feature",
    "chars": 1173,
    "preview": "Feature: Device API -- Request device settings\n  Test the endpoint used to fetch the settings from a device\n\n  Scenario:"
  },
  {
    "path": "api/public/tests/features/steps/common.py",
    "chars": 3678,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_email.py",
    "chars": 3866,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_location.py",
    "chars": 4620,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_metrics.py",
    "chars": 2475,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_pairing.py",
    "chars": 8239,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_refresh_token.py",
    "chars": 2543,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_skill_manifest.py",
    "chars": 8718,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/device_skill_settings.py",
    "chars": 8326,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/get_device.py",
    "chars": 6304,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/get_device_settings.py",
    "chars": 5402,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/get_device_subscription.py",
    "chars": 3513,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/transcribe_audio.py",
    "chars": 4005,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/wake_word_file.py",
    "chars": 5526,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/steps/wolfram_alpha.py",
    "chars": 2340,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/public/tests/features/transcribe_audio.feature",
    "chars": 814,
    "preview": "Feature: Transcribe audio data\n  Test the integration with audio transcription service providers\n\n  @stt\n  Scenario: Tra"
  },
  {
    "path": "api/public/tests/features/wake_word_file_upload.feature",
    "chars": 1129,
    "preview": "Feature: Device API -- Upload wake word samples\n  Users that opted in to the Open Dataset Agreement will have files cont"
  },
  {
    "path": "api/public/tests/features/wolfram_alpha.feature",
    "chars": 1148,
    "preview": "Feature: Device API -- Integration with Wolfram Alpha API\n  Mycroft Core uses Wolfram Alpha as a \"fallback\".  When a use"
  },
  {
    "path": "api/public/uwsgi.ini",
    "chars": 131,
    "preview": "[uwsgi]\nmaster = true\nmodule = public_api.api:public\nprocesses = 10\nsocket = :5000\ndie-on-term = true\nlazy = true\nlazy-a"
  },
  {
    "path": "api/sso/Dockerfile",
    "chars": 827,
    "preview": "# Docker config for the Selene skill service\n\n# The selene-shared parent image contains all the common Docker configs fo"
  },
  {
    "path": "api/sso/pyproject.toml",
    "chars": 517,
    "preview": "[tool.poetry]\nname = \"sso\"\nversion = \"0.1.0\"\ndescription = \"Single Sign on for all Selene applications\"\nauthors = [\"Chri"
  },
  {
    "path": "api/sso/sso_api/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/api.py",
    "chars": 3651,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/__init__.py",
    "chars": 1235,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/authenticate_internal.py",
    "chars": 2543,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/github_token.py",
    "chars": 1183,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/logout.py",
    "chars": 1570,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/password_change.py",
    "chars": 1410,
    "preview": "#  Mycroft Server - Backend\n#  Copyright (c) 2022 Mycroft AI Inc\n#  SPDX-License-Identifier: \tAGPL-3.0-or-later\n#  #\n#  "
  },
  {
    "path": "api/sso/sso_api/endpoints/password_reset.py",
    "chars": 3317,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/validate_federated.py",
    "chars": 3462,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/sso_api/endpoints/validate_token.py",
    "chars": 1464,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/add_account.feature",
    "chars": 1443,
    "preview": "Feature: Single Sign On API -- Add a new account\n  Test the API call to add an account to the database.\n\n  Scenario: Suc"
  },
  {
    "path": "api/sso/tests/features/agreements.feature",
    "chars": 529,
    "preview": "Feature: Single Sign On API -- Get the active agreements\n  We need to be able to retrieve an agreement and display it on"
  },
  {
    "path": "api/sso/tests/features/environment.py",
    "chars": 2709,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/federated_login.feature",
    "chars": 1217,
    "preview": "Feature: Single Sign On API -- Federated login\n  User signs into a selene web app after authenticating with a 3rd party."
  },
  {
    "path": "api/sso/tests/features/internal_login.feature",
    "chars": 767,
    "preview": "Feature: Single Sign On API -- Internal login\n  User signs into a selene web app with an email address and password (rat"
  },
  {
    "path": "api/sso/tests/features/logout.feature",
    "chars": 360,
    "preview": "Feature: Single Sign On API -- Logout\n  Regardless of how a user logs in, logging out consists of expiring the\n  tokens "
  },
  {
    "path": "api/sso/tests/features/password_change.feature",
    "chars": 592,
    "preview": "Feature: Single Sign On API -- Password reset\n  The only way a user can change their password in the single sign on appl"
  },
  {
    "path": "api/sso/tests/features/steps/add_account.py",
    "chars": 5224,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/steps/agreements.py",
    "chars": 1468,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/steps/common.py",
    "chars": 1866,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/steps/login.py",
    "chars": 2861,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/steps/logout.py",
    "chars": 1998,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/tests/features/steps/password_change.py",
    "chars": 1775,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "api/sso/uwsgi.ini",
    "chars": 124,
    "preview": "[uwsgi]\nmaster = true\nmodule = sso_api.api:sso\nprocesses = 4\nsocket = :5000\ndie-on-term = true\nlazy = true\nlazy-apps = t"
  },
  {
    "path": "batch/job_scheduler/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/job_scheduler/jobs.py",
    "chars": 5624,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/pyproject.toml",
    "chars": 420,
    "preview": "[tool.poetry]\nname = \"batch\"\nversion = \"0.1.0\"\ndescription = \"Selene batch scripts and scheduler\"\nauthors = [\"Chris Veil"
  },
  {
    "path": "batch/script/__init__.py",
    "chars": 816,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/daily_report.py",
    "chars": 2565,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/delete_wake_word_files.py",
    "chars": 6102,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/designate_wake_word_files.py",
    "chars": 6882,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/load_skill_display_data.py",
    "chars": 3547,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/move_wake_word_files.py",
    "chars": 4136,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2020 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/parse_core_metrics.py",
    "chars": 4872,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/partition_api_metrics.py",
    "chars": 1650,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/test_scheduler.py",
    "chars": 2296,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "batch/script/update_device_last_contact.py",
    "chars": 2427,
    "preview": "# Mycroft Server - Backend\n# Copyright (C) 2019 Mycroft AI Inc\n# SPDX-License-Identifier: \tAGPL-3.0-or-later\n#\n# This fi"
  },
  {
    "path": "db/mycroft/account_schema/create_schema.sql",
    "chars": 148,
    "preview": "-- create the schema that will be used to store user data\n-- took out the \"e\" in \"user\" because \"user\" is a Postgres key"
  },
  {
    "path": "db/mycroft/account_schema/data/membership.sql",
    "chars": 220,
    "preview": "INSERT INTO\n    account.membership (type, rate, rate_period, stripe_plan)\nVALUES\n    ('Monthly Membership', 1.99, 'month"
  },
  {
    "path": "db/mycroft/account_schema/grants.sql",
    "chars": 121,
    "preview": "GRANT USAGE ON SCHEMA account TO selene;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA account TO selene;"
  },
  {
    "path": "db/mycroft/account_schema/tables/account.sql",
    "chars": 407,
    "preview": "CREATE TABLE account.account (\n    id                      uuid        PRIMARY KEY\n            DEFAULT gen_random_uuid()"
  },
  {
    "path": "db/mycroft/account_schema/tables/account_agreement.sql",
    "chars": 454,
    "preview": "CREATE TABLE account.account_agreement (\n    id                  uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    "
  },
  {
    "path": "db/mycroft/account_schema/tables/account_membership.sql",
    "chars": 851,
    "preview": "CREATE TABLE account.account_membership (\n    id                      uuid                    PRIMARY KEY\n            DE"
  },
  {
    "path": "db/mycroft/account_schema/tables/agreement.sql",
    "chars": 440,
    "preview": "CREATE TABLE account.agreement (\n    id              uuid            PRIMARY KEY DEFAULT gen_random_uuid(),\n    agreemen"
  },
  {
    "path": "db/mycroft/account_schema/tables/membership.sql",
    "chars": 454,
    "preview": "CREATE TABLE account.membership (\n    id              uuid                    PRIMARY KEY\n            DEFAULT gen_random"
  },
  {
    "path": "db/mycroft/create_extensions.sql",
    "chars": 55,
    "preview": "CREATE EXTENSION pgcrypto;\nCREATE EXTENSION btree_gist;"
  },
  {
    "path": "db/mycroft/create_mycroft_db.sql",
    "chars": 69,
    "preview": "CREATE DATABASE mycroft WITH TEMPLATE mycroft_template OWNER selene;\n"
  },
  {
    "path": "db/mycroft/create_roles.sql",
    "chars": 128,
    "preview": "-- create the roles that will be used by selene applications\nCREATE ROLE selene WITH SUPERUSER LOGIN ENCRYPTED PASSWORD "
  },
  {
    "path": "db/mycroft/create_template_db.sql",
    "chars": 54,
    "preview": "CREATE DATABASE mycroft_template WITH OWNER = selene;\n"
  },
  {
    "path": "db/mycroft/device_schema/create_schema.sql",
    "chars": 147,
    "preview": "-- create the schema that will be used to store user data\n-- took out the \"e\" in \"user\" because \"user\" is a Postgres key"
  },
  {
    "path": "db/mycroft/device_schema/data/text_to_speech.sql",
    "chars": 248,
    "preview": "INSERT INTO\n    device.text_to_speech (setting_name, display_name,  engine)\nVALUES\n    ('ap', 'British Male', 'mimic'),\n"
  },
  {
    "path": "db/mycroft/device_schema/get_device_defaults_for_city.sql",
    "chars": 94,
    "preview": "SELECT\n    id,\n    city_id\nFROM\n    device.account_defaults\nWHERE\n    city_id IN %(city_ids)s\n"
  },
  {
    "path": "db/mycroft/device_schema/get_device_geographies_for_city.sql",
    "chars": 87,
    "preview": "SELECT\n    id,\n    city_id\nFROM\n    device.geography\nWHERE\n    city_id IN %(city_ids)s\n"
  },
  {
    "path": "db/mycroft/device_schema/grants.sql",
    "chars": 119,
    "preview": "GRANT USAGE ON SCHEMA device TO selene;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA device TO selene;\n"
  },
  {
    "path": "db/mycroft/device_schema/tables/account_defaults.sql",
    "chars": 862,
    "preview": "-- Account level preferences that pertain to device function.\nCREATE TABLE device.account_defaults (\n    id             "
  },
  {
    "path": "db/mycroft/device_schema/tables/account_preferences.sql",
    "chars": 703,
    "preview": "-- Account level preferences that pertain to device function.\nCREATE TABLE device.account_preferences (\n    id          "
  },
  {
    "path": "db/mycroft/device_schema/tables/category.sql",
    "chars": 304,
    "preview": "CREATE TABLE device.category (\n    id          uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    account_id  uuid  "
  },
  {
    "path": "db/mycroft/device_schema/tables/device.sql",
    "chars": 1051,
    "preview": "CREATE TABLE device.device (\n    id                  uuid            PRIMARY KEY\n            DEFAULT gen_random_uuid(),\n"
  },
  {
    "path": "db/mycroft/device_schema/tables/device_skill.sql",
    "chars": 802,
    "preview": "CREATE TABLE device.device_skill (\n    id                      uuid        PRIMARY KEY\n        DEFAULT gen_random_uuid()"
  },
  {
    "path": "db/mycroft/device_schema/tables/geography.sql",
    "chars": 683,
    "preview": "CREATE TABLE device.geography (\n    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    account_id   "
  },
  {
    "path": "db/mycroft/device_schema/tables/pantacor_config.sql",
    "chars": 459,
    "preview": "CREATE TABLE device.pantacor_config (\n    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    device_"
  },
  {
    "path": "db/mycroft/device_schema/tables/skill_setting.sql",
    "chars": 419,
    "preview": "CREATE TABLE device.skill_setting (\n    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    device_sk"
  },
  {
    "path": "db/mycroft/device_schema/tables/text_to_speech.sql",
    "chars": 331,
    "preview": "CREATE TABLE device.text_to_speech (\n    id              uuid            PRIMARY KEY DEFAULT gen_random_uuid(),\n    sett"
  },
  {
    "path": "db/mycroft/device_schema/tables/wake_word.sql",
    "chars": 412,
    "preview": "CREATE TABLE device.wake_word (\n    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),\n    setting_name "
  },
  {
    "path": "db/mycroft/device_schema/tables/wake_word_settings.sql",
    "chars": 566,
    "preview": "-- Settings for wake words using the Pocketsphinx engine\nCREATE TABLE device.wake_word_settings (\n    id                "
  },
  {
    "path": "db/mycroft/drop_extensions.sql",
    "chars": 71,
    "preview": "DROP EXTENSION IF EXISTS pgcrypto;\nDROP EXTENSION IF EXISTS btree_gist;"
  },
  {
    "path": "db/mycroft/drop_mycroft_db.sql",
    "chars": 33,
    "preview": "DROP DATABASE IF EXISTS mycroft;\n"
  }
]

// ... and 338 more files (download for full content)

About this extraction

This page contains the full source code of the MycroftAI/selene-backend GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 538 files (1001.5 KB), approximately 242.5k tokens, and a symbol index with 1296 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!