usenadia 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- usenadia-0.1.0/.gitignore +6 -0
- usenadia-0.1.0/LICENSE +21 -0
- usenadia-0.1.0/PKG-INFO +98 -0
- usenadia-0.1.0/README.md +85 -0
- usenadia-0.1.0/pyproject.toml +33 -0
- usenadia-0.1.0/tests/test_facade.py +131 -0
- usenadia-0.1.0/usenadia/__init__.py +278 -0
- usenadia-0.1.0/usenadia/generated/__init__.py +8 -0
- usenadia-0.1.0/usenadia/generated/api/__init__.py +1 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/__init__.py +1 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/call_video_function.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/change_post_status.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/check_connection.py +144 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/create_automation_flow.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/create_content.py +181 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/create_post.py +183 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/create_webhook.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_automation_flow.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_automation_pose.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_channel.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_comment.py +185 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_content.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_post.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_post_group.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/delete_webhook.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/find_slot.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/generate_video.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_account_history.py +252 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_automation_flow.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_automation_pose.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_channel_analytics.py +197 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_channel_settings.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_content.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_contents_performance.py +146 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_conversation.py +190 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_hook_forms.py +265 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_music_track.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_post_analytics.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_video_history.py +252 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/get_webhook.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/hide_comment.py +206 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/import_hooks.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/import_music.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/link_content_post.py +208 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_automation_flow_poses.py +178 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_automation_flows.py +151 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_automation_pose_runs.py +199 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_channel_automation_poses.py +198 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_channel_media.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_channel_stories.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_channels.py +181 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_contents.py +239 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_conversations.py +197 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_groups.py +149 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_hook_families.py +196 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_media_comments.py +190 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_missing_media.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_music.py +252 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_music_pillars.py +152 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_notifications.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_posts.py +236 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_video_performance.py +209 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_webhook_events.py +144 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/list_webhooks.py +149 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/move_content.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/poll_automation_pose.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/poll_inbox.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/pose_automation_flow.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/preview_automation_email.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/replace_hook_families.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/replay_automation_pose.py +199 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/reply_to_comment.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/run_channel_tool.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/search_hooks.py +267 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/send_automation_test_email.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/send_direct_message.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/set_automation_flow_status.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/set_post_release_id.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/start_channel_connection.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/subscribe_inbox_webhook.py +171 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/update_automation_flow.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/update_automation_pose.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/update_content.py +192 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/upload_file.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/upload_from_url.py +176 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/upload_music_file.py +197 -0
- usenadia-0.1.0/usenadia/generated/api/public_api/upsert_content.py +188 -0
- usenadia-0.1.0/usenadia/generated/client.py +280 -0
- usenadia-0.1.0/usenadia/generated/errors.py +16 -0
- usenadia-0.1.0/usenadia/generated/models/__init__.py +335 -0
- usenadia-0.1.0/usenadia/generated/models/account_history_item_dto.py +86 -0
- usenadia-0.1.0/usenadia/generated/models/analytics_metric_dto.py +94 -0
- usenadia-0.1.0/usenadia/generated/models/analytics_point_dto.py +70 -0
- usenadia-0.1.0/usenadia/generated/models/api_error_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_counts_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_dto.py +97 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_dto_definition.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_issue_dto.py +116 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_issue_dto_level.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_issue_dto_params.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_poll_result_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_pose_response_dto.py +206 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_pose_response_dto_source.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_pose_response_dto_variables.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_replay_result_dto.py +119 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_response_dto.py +173 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_response_dto_definition.py +50 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_response_dto_status.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_run_response_dto.py +269 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_run_response_dto_status.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_status_dto.py +63 -0
- usenadia-0.1.0/usenadia/generated/models/automation_flow_status_dto_status.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/automation_pose_counts_dto.py +93 -0
- usenadia-0.1.0/usenadia/generated/models/automation_pose_flow_dto.py +79 -0
- usenadia-0.1.0/usenadia/generated/models/automation_pose_flow_dto_status.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/call_video_function_response_201.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/change_post_status_dto.py +63 -0
- usenadia-0.1.0/usenadia/generated/models/change_post_status_dto_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/content_channel_performance_dto.py +368 -0
- usenadia-0.1.0/usenadia/generated/models/content_dto.py +408 -0
- usenadia-0.1.0/usenadia/generated/models/content_dto_resource_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/content_dto_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/content_list_item_dto.py +441 -0
- usenadia-0.1.0/usenadia/generated/models/content_list_item_dto_resource_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/content_list_item_dto_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/content_performance_dto.py +175 -0
- usenadia-0.1.0/usenadia/generated/models/content_post_dto.py +109 -0
- usenadia-0.1.0/usenadia/generated/models/content_post_dto_state.py +12 -0
- usenadia-0.1.0/usenadia/generated/models/content_upsert_dto.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/create_content_dto.py +202 -0
- usenadia-0.1.0/usenadia/generated/models/create_content_dto_resource_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/create_content_dto_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/create_post_result_item_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/create_public_webhook_dto.py +133 -0
- usenadia-0.1.0/usenadia/generated/models/create_public_webhook_dto_events_item.py +12 -0
- usenadia-0.1.0/usenadia/generated/models/deleted_id_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/email_preview_dto.py +143 -0
- usenadia-0.1.0/usenadia/generated/models/email_preview_dto_response.py +122 -0
- usenadia-0.1.0/usenadia/generated/models/email_preview_dto_response_params.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/email_preview_dto_response_sender.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/email_sent_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/email_test_dto.py +163 -0
- usenadia-0.1.0/usenadia/generated/models/find_slot_dto.py +62 -0
- usenadia-0.1.0/usenadia/generated/models/generated_video_dto.py +119 -0
- usenadia-0.1.0/usenadia/generated/models/get_contents_performance_response_200.py +62 -0
- usenadia-0.1.0/usenadia/generated/models/get_hook_forms_language.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/get_post_analytics_response_200.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/group_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/hide_comment_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/history_page_dto.py +116 -0
- usenadia-0.1.0/usenadia/generated/models/hook_dto.py +311 -0
- usenadia-0.1.0/usenadia/generated/models/hook_families_import_dto.py +83 -0
- usenadia-0.1.0/usenadia/generated/models/hook_family_dto.py +220 -0
- usenadia-0.1.0/usenadia/generated/models/hook_family_stat_dto.py +212 -0
- usenadia-0.1.0/usenadia/generated/models/hook_form_dto.py +186 -0
- usenadia-0.1.0/usenadia/generated/models/hook_import_item_dto.py +211 -0
- usenadia-0.1.0/usenadia/generated/models/hooks_import_dto.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/hooks_import_result_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/hooks_search_result_dto.py +83 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_conversation_dto.py +128 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_conversation_dto_inbox.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_conversation_dto_messages.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_conversation_dto_participants.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_message_dto.py +102 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_participant_dto.py +72 -0
- usenadia-0.1.0/usenadia/generated/models/inbox_poll_result_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/instagram_comment_author_dto.py +70 -0
- usenadia-0.1.0/usenadia/generated/models/instagram_comment_dto.py +123 -0
- usenadia-0.1.0/usenadia/generated/models/instagram_media_dto.py +145 -0
- usenadia-0.1.0/usenadia/generated/models/integration.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/integration_list_item_dto.py +138 -0
- usenadia-0.1.0/usenadia/generated/models/integration_settings_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/integration_settings_output_dto.py +106 -0
- usenadia-0.1.0/usenadia/generated/models/integration_settings_output_dto_settings.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/integration_tool_dto.py +91 -0
- usenadia-0.1.0/usenadia/generated/models/integration_tool_param_dto.py +77 -0
- usenadia-0.1.0/usenadia/generated/models/integration_trigger_dto.py +85 -0
- usenadia-0.1.0/usenadia/generated/models/integration_trigger_dto_data.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/integration_trigger_result_dto.py +71 -0
- usenadia-0.1.0/usenadia/generated/models/integration_trigger_result_dto_output.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/is_connected_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/link_content_post_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/list_contents_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/list_music_pillar.py +11 -0
- usenadia-0.1.0/usenadia/generated/models/list_video_performance_response_200.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/media_dto.py +89 -0
- usenadia-0.1.0/usenadia/generated/models/media_response_dto.py +119 -0
- usenadia-0.1.0/usenadia/generated/models/missing_content_item_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/move_content_dto.py +73 -0
- usenadia-0.1.0/usenadia/generated/models/move_content_dto_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/music_import_dto.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/music_import_item_dto.py +238 -0
- usenadia-0.1.0/usenadia/generated/models/music_import_item_dto_pillar.py +11 -0
- usenadia-0.1.0/usenadia/generated/models/music_import_item_dto_verdict.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/music_import_result_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/music_list_result_dto.py +91 -0
- usenadia-0.1.0/usenadia/generated/models/music_pillar_dto.py +85 -0
- usenadia-0.1.0/usenadia/generated/models/music_track_dto.py +318 -0
- usenadia-0.1.0/usenadia/generated/models/notification_dto.py +100 -0
- usenadia-0.1.0/usenadia/generated/models/notifications_page_dto.py +107 -0
- usenadia-0.1.0/usenadia/generated/models/ok_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/pose_automation_flow_dto.py +125 -0
- usenadia-0.1.0/usenadia/generated/models/pose_automation_flow_dto_kind.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/pose_automation_flow_dto_variables.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/post.py +114 -0
- usenadia-0.1.0/usenadia/generated/models/post_content.py +99 -0
- usenadia-0.1.0/usenadia/generated/models/post_dto.py +219 -0
- usenadia-0.1.0/usenadia/generated/models/post_dto_creation_method.py +11 -0
- usenadia-0.1.0/usenadia/generated/models/post_dto_state.py +12 -0
- usenadia-0.1.0/usenadia/generated/models/post_integration_summary_dto.py +91 -0
- usenadia-0.1.0/usenadia/generated/models/post_list_dto.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/post_settings.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/post_status_dto.py +71 -0
- usenadia-0.1.0/usenadia/generated/models/post_status_dto_state.py +11 -0
- usenadia-0.1.0/usenadia/generated/models/post_tag_dto.py +67 -0
- usenadia-0.1.0/usenadia/generated/models/post_tag_dto_tag.py +47 -0
- usenadia-0.1.0/usenadia/generated/models/public_create_post_dto.py +160 -0
- usenadia-0.1.0/usenadia/generated/models/public_create_post_dto_creation_method.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/public_create_post_dto_type.py +11 -0
- usenadia-0.1.0/usenadia/generated/models/replay_pose_dto.py +72 -0
- usenadia-0.1.0/usenadia/generated/models/reply_to_comment_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/search_hooks_language.py +10 -0
- usenadia-0.1.0/usenadia/generated/models/send_message_dto.py +89 -0
- usenadia-0.1.0/usenadia/generated/models/social_auth_url_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/success_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/tags.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/update_automation_flow_dto.py +107 -0
- usenadia-0.1.0/usenadia/generated/models/update_automation_flow_dto_definition.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/update_automation_flow_pose_dto.py +98 -0
- usenadia-0.1.0/usenadia/generated/models/update_automation_flow_pose_dto_variables.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/update_content_dto.py +300 -0
- usenadia-0.1.0/usenadia/generated/models/update_content_dto_resource_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/update_release_id_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/upload_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/models/upload_file_body.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/upload_music_file_body.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/upsert_content_dto.py +200 -0
- usenadia-0.1.0/usenadia/generated/models/upsert_content_dto_resource_status.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/upsert_content_dto_stage.py +14 -0
- usenadia-0.1.0/usenadia/generated/models/video_dto.py +85 -0
- usenadia-0.1.0/usenadia/generated/models/video_dto_custom_params.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/video_dto_output.py +9 -0
- usenadia-0.1.0/usenadia/generated/models/video_function_dto.py +93 -0
- usenadia-0.1.0/usenadia/generated/models/video_function_dto_params.py +45 -0
- usenadia-0.1.0/usenadia/generated/models/video_history_page_dto.py +143 -0
- usenadia-0.1.0/usenadia/generated/models/video_identity_dto.py +123 -0
- usenadia-0.1.0/usenadia/generated/models/webhook_created_dto.py +135 -0
- usenadia-0.1.0/usenadia/generated/models/webhook_dto.py +125 -0
- usenadia-0.1.0/usenadia/generated/models/webhook_event_dto.py +69 -0
- usenadia-0.1.0/usenadia/generated/models/webhook_event_list_dto.py +75 -0
- usenadia-0.1.0/usenadia/generated/models/webhooks_integration_dto.py +61 -0
- usenadia-0.1.0/usenadia/generated/types.py +54 -0
- usenadia-0.1.0/uv.lock +127 -0
usenadia-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincent Ventalon (UseNadia)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
usenadia-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: usenadia
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the UseNadia public API — schedule posts, manage channels, run the Instagram comment/story → DM machinery
|
|
5
|
+
Project-URL: Homepage, https://usenadia.com
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: attrs>=22.2
|
|
10
|
+
Requires-Dist: httpx<1,>=0.23
|
|
11
|
+
Requires-Dist: python-dateutil>=2.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# usenadia
|
|
15
|
+
|
|
16
|
+
Python SDK for the **UseNadia** public API (`/public/v1`) — schedule posts,
|
|
17
|
+
manage channels, and drive the Instagram comment/story → DM machinery.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install usenadia
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from usenadia import Nadia, NadiaError
|
|
25
|
+
|
|
26
|
+
nadia = Nadia(api_key=os.environ["USENADIA_API_KEY"])
|
|
27
|
+
|
|
28
|
+
channels = nadia.integrations()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
nadia.post({
|
|
32
|
+
"type": "schedule",
|
|
33
|
+
"date": "2026-10-01T09:00:00.000Z",
|
|
34
|
+
"shortLink": False,
|
|
35
|
+
"tags": [],
|
|
36
|
+
"posts": [{
|
|
37
|
+
"integration": {"id": channels[0]["id"]},
|
|
38
|
+
"value": [{"content": "Hello from the SDK", "image": []}],
|
|
39
|
+
"settings": {},
|
|
40
|
+
}],
|
|
41
|
+
})
|
|
42
|
+
except NadiaError as err:
|
|
43
|
+
print(err.status, err) # 402 You have reached your limit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The content board
|
|
47
|
+
|
|
48
|
+
Every card of the board in the app is reachable: create one from anywhere
|
|
49
|
+
(a voice note, a Notion page, a DM), move it along, read its numbers once
|
|
50
|
+
it is published.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
card = nadia.create_content(
|
|
54
|
+
"Three mistakes when moving to Malta",
|
|
55
|
+
hook="Nobody told me about the third one.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
nadia.move_content(card["id"], "SCRIPT")
|
|
59
|
+
nadia.update_content(card["id"], script="…")
|
|
60
|
+
|
|
61
|
+
# Scheduled with nadia.post({..., "posts": [{"group": "g-42", ...}]}):
|
|
62
|
+
nadia.link_content_post(card["id"], "g-42") # → PUBLISHED when it goes live
|
|
63
|
+
|
|
64
|
+
numbers = nadia.contents_performance() # keyed by card id
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Columns, in order: `IDEA`, `SCRIPT`, `VALIDATED`, `TO_SHOOT`, `TO_EDIT`,
|
|
68
|
+
`TO_SCHEDULE`, `PUBLISHED`.
|
|
69
|
+
|
|
70
|
+
## How it is built
|
|
71
|
+
|
|
72
|
+
Two layers, and the split is deliberate.
|
|
73
|
+
|
|
74
|
+
- **`usenadia/generated/`** — the transport. Generated from
|
|
75
|
+
`openapi.public.json` by
|
|
76
|
+
[`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client)
|
|
77
|
+
(httpx + attrs), covers every operation, never edited by hand, not
|
|
78
|
+
committed.
|
|
79
|
+
- **`usenadia/__init__.py`** — this SDK's own surface: the `Nadia` class,
|
|
80
|
+
sane defaults, typed errors, retries. Small, hand-written, reviewed.
|
|
81
|
+
|
|
82
|
+
Anything the facade does not wrap is reachable through `nadia.raw`, the
|
|
83
|
+
configured generated client.
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
Every response with a status ≥ 400 raises a `NadiaError` carrying `status`,
|
|
88
|
+
`body` and the API's `msg`. `err.is_transient` is true for 429 and 5xx — those
|
|
89
|
+
are retried automatically (twice by default, `Nadia(key, retries=0)` to turn
|
|
90
|
+
it off).
|
|
91
|
+
|
|
92
|
+
## Regenerating
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm run sdk:generate # from the repo root — regenerates both SDKs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Plan: `docs/roadmap/19-sdk-public-api.md`.
|
usenadia-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# usenadia
|
|
2
|
+
|
|
3
|
+
Python SDK for the **UseNadia** public API (`/public/v1`) — schedule posts,
|
|
4
|
+
manage channels, and drive the Instagram comment/story → DM machinery.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install usenadia
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from usenadia import Nadia, NadiaError
|
|
12
|
+
|
|
13
|
+
nadia = Nadia(api_key=os.environ["USENADIA_API_KEY"])
|
|
14
|
+
|
|
15
|
+
channels = nadia.integrations()
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
nadia.post({
|
|
19
|
+
"type": "schedule",
|
|
20
|
+
"date": "2026-10-01T09:00:00.000Z",
|
|
21
|
+
"shortLink": False,
|
|
22
|
+
"tags": [],
|
|
23
|
+
"posts": [{
|
|
24
|
+
"integration": {"id": channels[0]["id"]},
|
|
25
|
+
"value": [{"content": "Hello from the SDK", "image": []}],
|
|
26
|
+
"settings": {},
|
|
27
|
+
}],
|
|
28
|
+
})
|
|
29
|
+
except NadiaError as err:
|
|
30
|
+
print(err.status, err) # 402 You have reached your limit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The content board
|
|
34
|
+
|
|
35
|
+
Every card of the board in the app is reachable: create one from anywhere
|
|
36
|
+
(a voice note, a Notion page, a DM), move it along, read its numbers once
|
|
37
|
+
it is published.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
card = nadia.create_content(
|
|
41
|
+
"Three mistakes when moving to Malta",
|
|
42
|
+
hook="Nobody told me about the third one.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
nadia.move_content(card["id"], "SCRIPT")
|
|
46
|
+
nadia.update_content(card["id"], script="…")
|
|
47
|
+
|
|
48
|
+
# Scheduled with nadia.post({..., "posts": [{"group": "g-42", ...}]}):
|
|
49
|
+
nadia.link_content_post(card["id"], "g-42") # → PUBLISHED when it goes live
|
|
50
|
+
|
|
51
|
+
numbers = nadia.contents_performance() # keyed by card id
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Columns, in order: `IDEA`, `SCRIPT`, `VALIDATED`, `TO_SHOOT`, `TO_EDIT`,
|
|
55
|
+
`TO_SCHEDULE`, `PUBLISHED`.
|
|
56
|
+
|
|
57
|
+
## How it is built
|
|
58
|
+
|
|
59
|
+
Two layers, and the split is deliberate.
|
|
60
|
+
|
|
61
|
+
- **`usenadia/generated/`** — the transport. Generated from
|
|
62
|
+
`openapi.public.json` by
|
|
63
|
+
[`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client)
|
|
64
|
+
(httpx + attrs), covers every operation, never edited by hand, not
|
|
65
|
+
committed.
|
|
66
|
+
- **`usenadia/__init__.py`** — this SDK's own surface: the `Nadia` class,
|
|
67
|
+
sane defaults, typed errors, retries. Small, hand-written, reviewed.
|
|
68
|
+
|
|
69
|
+
Anything the facade does not wrap is reachable through `nadia.raw`, the
|
|
70
|
+
configured generated client.
|
|
71
|
+
|
|
72
|
+
## Errors
|
|
73
|
+
|
|
74
|
+
Every response with a status ≥ 400 raises a `NadiaError` carrying `status`,
|
|
75
|
+
`body` and the API's `msg`. `err.is_transient` is true for 429 and 5xx — those
|
|
76
|
+
are retried automatically (twice by default, `Nadia(key, retries=0)` to turn
|
|
77
|
+
it off).
|
|
78
|
+
|
|
79
|
+
## Regenerating
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm run sdk:generate # from the repo root — regenerates both SDKs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Plan: `docs/roadmap/19-sdk-public-api.md`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "usenadia"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for the UseNadia public API — schedule posts, manage channels, run the Instagram comment/story → DM machinery"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.23,<1",
|
|
10
|
+
"attrs>=22.2",
|
|
11
|
+
"python-dateutil>=2.8",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://usenadia.com"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["usenadia"]
|
|
23
|
+
|
|
24
|
+
# usenadia/generated is git-ignored (regenerated from the spec) and hatch
|
|
25
|
+
# leaves ignored files out: without this, the wheel ships a facade whose
|
|
26
|
+
# generated client is missing.
|
|
27
|
+
[tool.hatch.build]
|
|
28
|
+
artifacts = ["usenadia/generated"]
|
|
29
|
+
|
|
30
|
+
# openapi-python-client is NOT a dependency of this package: it is a build
|
|
31
|
+
# tool, run isolated. Its output uses `typing.Self`, hence Python >= 3.11:
|
|
32
|
+
# uvx openapi-python-client generate ...
|
|
33
|
+
# See the repo-root `pnpm sdk:generate`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Offline tests of the hand-written layer — no network, no API key.
|
|
2
|
+
|
|
3
|
+
Same reason as the Node side: what this facade adds over a bare `requests`
|
|
4
|
+
call is error handling and retries, and a regression there would be silent.
|
|
5
|
+
|
|
6
|
+
The smoke tests against the real API (plan 19, phase 4) are a different thing
|
|
7
|
+
and live elsewhere: these run in CI with nothing configured.
|
|
8
|
+
|
|
9
|
+
uv run --python 3.11 --with httpx --with pytest pytest tests/
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from usenadia import DEFAULT_BASE_URL, Nadia, NadiaError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def client_with(*responses):
|
|
21
|
+
"""An httpx client that replays the queued responses and records requests."""
|
|
22
|
+
calls = []
|
|
23
|
+
|
|
24
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
25
|
+
calls.append(request)
|
|
26
|
+
return responses[min(len(calls) - 1, len(responses) - 1)]
|
|
27
|
+
|
|
28
|
+
transport = httpx.MockTransport(handler)
|
|
29
|
+
return (
|
|
30
|
+
httpx.Client(
|
|
31
|
+
base_url=DEFAULT_BASE_URL,
|
|
32
|
+
headers={"Authorization": "ma-clef"},
|
|
33
|
+
transport=transport,
|
|
34
|
+
),
|
|
35
|
+
calls,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_sends_the_api_key_and_hits_the_documented_base_url():
|
|
40
|
+
http, calls = client_with(httpx.Response(200, json=[{"id": "c1", "name": "Channel"}]))
|
|
41
|
+
nadia = Nadia("ma-clef", client=http)
|
|
42
|
+
|
|
43
|
+
assert nadia.integrations() == [{"id": "c1", "name": "Channel"}]
|
|
44
|
+
assert str(calls[0].url) == f"{DEFAULT_BASE_URL}/public/v1/integrations"
|
|
45
|
+
assert calls[0].method == "GET"
|
|
46
|
+
assert calls[0].headers["authorization"] == "ma-clef"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_a_402_raises_with_the_api_message():
|
|
50
|
+
http, _ = client_with(httpx.Response(402, json={"msg": "You have reached your limit"}))
|
|
51
|
+
nadia = Nadia("ma-clef", client=http)
|
|
52
|
+
|
|
53
|
+
with pytest.raises(NadiaError) as excinfo:
|
|
54
|
+
nadia.post({"type": "schedule"})
|
|
55
|
+
|
|
56
|
+
assert excinfo.value.status == 402
|
|
57
|
+
assert str(excinfo.value) == "You have reached your limit"
|
|
58
|
+
assert excinfo.value.is_transient is False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_a_401_raises_so_a_bad_key_is_not_silent():
|
|
62
|
+
http, _ = client_with(httpx.Response(401, json={"msg": "Unauthorized"}))
|
|
63
|
+
nadia = Nadia("mauvaise-clef", client=http)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(NadiaError) as excinfo:
|
|
66
|
+
nadia.is_connected()
|
|
67
|
+
|
|
68
|
+
assert excinfo.value.status == 401
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_retries_a_500_then_succeeds():
|
|
72
|
+
http, calls = client_with(
|
|
73
|
+
httpx.Response(500, json={"msg": "boom"}),
|
|
74
|
+
httpx.Response(200, json={"date": "2026-10-01T09:00:00.000Z"}),
|
|
75
|
+
)
|
|
76
|
+
nadia = Nadia("ma-clef", client=http)
|
|
77
|
+
|
|
78
|
+
assert nadia.find_slot("c1") == {"date": "2026-10-01T09:00:00.000Z"}
|
|
79
|
+
assert len(calls) == 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_gives_up_after_the_configured_retries():
|
|
83
|
+
http, calls = client_with(httpx.Response(503, json={"msg": "boom"}))
|
|
84
|
+
nadia = Nadia("ma-clef", client=http, retries=1)
|
|
85
|
+
|
|
86
|
+
with pytest.raises(NadiaError) as excinfo:
|
|
87
|
+
nadia.groups()
|
|
88
|
+
|
|
89
|
+
assert excinfo.value.status == 503
|
|
90
|
+
assert len(calls) == 2
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_an_empty_body_is_not_a_json_error():
|
|
94
|
+
http, _ = client_with(httpx.Response(204))
|
|
95
|
+
nadia = Nadia("ma-clef", client=http)
|
|
96
|
+
|
|
97
|
+
assert nadia.delete_post("p1") is None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_an_error_without_the_msg_shape_still_raises_cleanly():
|
|
101
|
+
http, _ = client_with(httpx.Response(400, text="<html>nginx</html>"))
|
|
102
|
+
nadia = Nadia("ma-clef", client=http)
|
|
103
|
+
|
|
104
|
+
with pytest.raises(NadiaError) as excinfo:
|
|
105
|
+
nadia.groups()
|
|
106
|
+
|
|
107
|
+
assert excinfo.value.status == 400
|
|
108
|
+
assert excinfo.value.body == "<html>nginx</html>"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_an_empty_api_key_is_refused_up_front():
|
|
112
|
+
with pytest.raises(ValueError):
|
|
113
|
+
Nadia("")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_the_board_creates_then_moves_a_card_with_only_a_stage():
|
|
117
|
+
http, calls = client_with(
|
|
118
|
+
httpx.Response(201, json={"id": "k1", "title": "Idée", "stage": "IDEA"}),
|
|
119
|
+
httpx.Response(200, json={"id": "k1", "stage": "SCRIPT"}),
|
|
120
|
+
)
|
|
121
|
+
nadia = Nadia("ma-clef", client=http)
|
|
122
|
+
|
|
123
|
+
card = nadia.create_content("Idée", hook="Accroche")
|
|
124
|
+
nadia.move_content(card["id"], "SCRIPT")
|
|
125
|
+
|
|
126
|
+
assert calls[0].method == "POST"
|
|
127
|
+
assert str(calls[0].url) == f"{DEFAULT_BASE_URL}/public/v1/contents"
|
|
128
|
+
assert json.loads(calls[0].content) == {"title": "Idée", "hook": "Accroche"}
|
|
129
|
+
assert calls[1].method == "PUT"
|
|
130
|
+
assert str(calls[1].url) == f"{DEFAULT_BASE_URL}/public/v1/contents/k1/move"
|
|
131
|
+
assert json.loads(calls[1].content) == {"stage": "SCRIPT"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""UseNadia Python SDK.
|
|
2
|
+
|
|
3
|
+
Two layers, on purpose (plan 19) — this module is the second one.
|
|
4
|
+
|
|
5
|
+
usenadia/generated/ transport, regenerated from ``openapi.public.json``
|
|
6
|
+
by openapi-python-client, covers every operation,
|
|
7
|
+
never edited by hand, not committed.
|
|
8
|
+
usenadia/__init__.py this file: the small, hand-written surface that makes
|
|
9
|
+
the common calls pleasant, and that we take
|
|
10
|
+
responsibility for.
|
|
11
|
+
|
|
12
|
+
Anything the facade does not wrap stays reachable through :attr:`Nadia.raw`,
|
|
13
|
+
the configured generated client.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Callable, Mapping, Optional, TypeVar
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
__all__ = ["Nadia", "NadiaError", "DEFAULT_BASE_URL"]
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = "https://app.usenadia.com/api"
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NadiaError(Exception):
|
|
31
|
+
"""Raised for every response with a status >= 400."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, status: int, body: Any, message: Optional[str] = None):
|
|
34
|
+
self.status = status
|
|
35
|
+
self.body = body
|
|
36
|
+
super().__init__(message or f"UseNadia API responded {status}")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_transient(self) -> bool:
|
|
40
|
+
"""True when retrying later could plausibly work (rate limit, server hiccup)."""
|
|
41
|
+
return self.status == 429 or self.status >= 500
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _message_of(body: Any) -> Optional[str]:
|
|
45
|
+
"""Read the API's ``{"msg": ...}`` error shape without assuming it is there."""
|
|
46
|
+
if isinstance(body, Mapping):
|
|
47
|
+
msg = body.get("msg")
|
|
48
|
+
if isinstance(msg, str):
|
|
49
|
+
return msg
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Nadia:
|
|
54
|
+
"""Client for the UseNadia public API.
|
|
55
|
+
|
|
56
|
+
>>> nadia = Nadia(api_key="...")
|
|
57
|
+
>>> channels = nadia.integrations()
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
api_key: str,
|
|
63
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
64
|
+
*,
|
|
65
|
+
retries: int = 2,
|
|
66
|
+
timeout: float = 30.0,
|
|
67
|
+
client: Optional[httpx.Client] = None,
|
|
68
|
+
):
|
|
69
|
+
if not api_key:
|
|
70
|
+
raise ValueError("An API key is required.")
|
|
71
|
+
|
|
72
|
+
self._api_key = api_key
|
|
73
|
+
self._retries = retries
|
|
74
|
+
self._typed: Any = None
|
|
75
|
+
self._client = client or httpx.Client(
|
|
76
|
+
base_url=base_url.rstrip("/"),
|
|
77
|
+
headers={"Authorization": api_key},
|
|
78
|
+
timeout=timeout,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------ #
|
|
82
|
+
|
|
83
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
84
|
+
"""Send a request, raise :class:`NadiaError` on non-2xx, retry the
|
|
85
|
+
transient ones. Every method below goes through here — that is the
|
|
86
|
+
whole point of the facade."""
|
|
87
|
+
last: Optional[NadiaError] = None
|
|
88
|
+
|
|
89
|
+
for attempt in range(self._retries + 1):
|
|
90
|
+
response = self._client.request(method, path, **kwargs)
|
|
91
|
+
|
|
92
|
+
if response.is_success:
|
|
93
|
+
if not response.content:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
return response.json()
|
|
97
|
+
except ValueError:
|
|
98
|
+
return response.content
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
body: Any = response.json()
|
|
102
|
+
except ValueError:
|
|
103
|
+
body = response.text
|
|
104
|
+
|
|
105
|
+
last = NadiaError(response.status_code, body, _message_of(body))
|
|
106
|
+
|
|
107
|
+
if not last.is_transient or attempt == self._retries:
|
|
108
|
+
raise last
|
|
109
|
+
|
|
110
|
+
time.sleep(2**attempt * 0.5)
|
|
111
|
+
|
|
112
|
+
assert last is not None
|
|
113
|
+
raise last
|
|
114
|
+
|
|
115
|
+
# -- the five calls upstream's Node SDK had, same names -------------- #
|
|
116
|
+
|
|
117
|
+
def post(self, body: Mapping[str, Any]) -> Any:
|
|
118
|
+
"""Schedule, publish or draft a post on one or more channels."""
|
|
119
|
+
return self._request("POST", "/public/v1/posts", json=body)
|
|
120
|
+
|
|
121
|
+
def post_list(self, **query: Any) -> Any:
|
|
122
|
+
"""List posts in a date range."""
|
|
123
|
+
return self._request("GET", "/public/v1/posts", params=query or None)
|
|
124
|
+
|
|
125
|
+
def upload(self, file: bytes, filename: str = "upload") -> Any:
|
|
126
|
+
"""Upload a file and get a URL usable in a post."""
|
|
127
|
+
return self._request(
|
|
128
|
+
"POST", "/public/v1/upload", files={"file": (filename, file)}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def integrations(self, group: Optional[str] = None) -> Any:
|
|
132
|
+
"""List your connected channels."""
|
|
133
|
+
return self._request(
|
|
134
|
+
"GET",
|
|
135
|
+
"/public/v1/integrations",
|
|
136
|
+
params={"group": group} if group else None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def delete_post(self, post_id: str) -> Any:
|
|
140
|
+
"""Delete a post and everything published with it."""
|
|
141
|
+
return self._request("DELETE", f"/public/v1/posts/{post_id}")
|
|
142
|
+
|
|
143
|
+
# -- the rest of our surface, which upstream never had --------------- #
|
|
144
|
+
|
|
145
|
+
def is_connected(self) -> bool:
|
|
146
|
+
"""Check the API key. Raises :class:`NadiaError` with status 401 if invalid."""
|
|
147
|
+
self._request("GET", "/public/v1/is-connected")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def find_slot(self, integration_id: str) -> Any:
|
|
151
|
+
"""Next free slot in a channel's schedule."""
|
|
152
|
+
return self._request("GET", f"/public/v1/find-slot/{integration_id}")
|
|
153
|
+
|
|
154
|
+
def groups(self) -> Any:
|
|
155
|
+
"""List your groups (brands)."""
|
|
156
|
+
return self._request("GET", "/public/v1/groups")
|
|
157
|
+
|
|
158
|
+
def inbox(self, integration_id: str, limit: Optional[int] = None) -> Any:
|
|
159
|
+
"""Direct message conversations of a channel."""
|
|
160
|
+
return self._request(
|
|
161
|
+
"GET",
|
|
162
|
+
f"/public/v1/inbox/{integration_id}",
|
|
163
|
+
params={"limit": limit} if limit else None,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# -- the content board: one card per video, idea to numbers --------- #
|
|
167
|
+
|
|
168
|
+
def contents(self, **filters: Any) -> Any:
|
|
169
|
+
"""The cards of the board, with the posts scheduled from each.
|
|
170
|
+
|
|
171
|
+
Filters combine: ``stage``, ``tag``, ``externalId``; ``fields``
|
|
172
|
+
(``"id,title,stage"``) leaves the scripts out.
|
|
173
|
+
"""
|
|
174
|
+
return self._request(
|
|
175
|
+
"GET", "/public/v1/contents", params=filters or None
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def content(self, content_id: str) -> Any:
|
|
179
|
+
"""One card."""
|
|
180
|
+
return self._request("GET", f"/public/v1/contents/{content_id}")
|
|
181
|
+
|
|
182
|
+
def create_content(self, title: str, **fields: Any) -> Any:
|
|
183
|
+
"""Create a card — in ``IDEA`` unless ``stage`` says otherwise.
|
|
184
|
+
|
|
185
|
+
Other fields: ``externalId``, ``hook``, ``hookText``, ``script``,
|
|
186
|
+
``notes``, ``tags``, ``resourceStatus``, ``resourceUrl``,
|
|
187
|
+
``resourceBrief``, ``integrationIds``, ``plannedAt``, ``parentId``.
|
|
188
|
+
"""
|
|
189
|
+
return self._request(
|
|
190
|
+
"POST", "/public/v1/contents", json={"title": title, **fields}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def upsert_content(self, external_id: str, title: str, **fields: Any) -> Any:
|
|
194
|
+
"""Create or update the card holding ``external_id`` (your own id).
|
|
195
|
+
|
|
196
|
+
Re-running an import never duplicates; an existing card keeps its
|
|
197
|
+
column. Returns ``{"created": bool, "content": {...}}``.
|
|
198
|
+
"""
|
|
199
|
+
return self._request(
|
|
200
|
+
"POST",
|
|
201
|
+
"/public/v1/contents/upsert",
|
|
202
|
+
json={"externalId": external_id, "title": title, **fields},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def update_content(self, content_id: str, **fields: Any) -> Any:
|
|
206
|
+
"""Update a card: only the fields passed change, ``None`` clears one."""
|
|
207
|
+
return self._request(
|
|
208
|
+
"PUT", f"/public/v1/contents/{content_id}", json=fields
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def move_content(
|
|
212
|
+
self, content_id: str, stage: str, position: Optional[float] = None
|
|
213
|
+
) -> Any:
|
|
214
|
+
"""Move a card to another column (to the bottom unless ``position`` is given)."""
|
|
215
|
+
body: dict = {"stage": stage}
|
|
216
|
+
if position is not None:
|
|
217
|
+
body["position"] = position
|
|
218
|
+
return self._request(
|
|
219
|
+
"PUT", f"/public/v1/contents/{content_id}/move", json=body
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def link_content_post(self, content_id: str, post_group: str) -> Any:
|
|
223
|
+
"""Tie a card to a post scheduled from it (the post's ``group``).
|
|
224
|
+
|
|
225
|
+
A card holds several posts (a TikTok draft, an Instagram post): each
|
|
226
|
+
call adds one. The card moves to ``PUBLISHED`` when the first of them
|
|
227
|
+
goes live.
|
|
228
|
+
"""
|
|
229
|
+
return self._request(
|
|
230
|
+
"PUT",
|
|
231
|
+
f"/public/v1/contents/{content_id}/post",
|
|
232
|
+
json={"postGroup": post_group},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def delete_content(self, content_id: str) -> Any:
|
|
236
|
+
"""Delete a card."""
|
|
237
|
+
return self._request("DELETE", f"/public/v1/contents/{content_id}")
|
|
238
|
+
|
|
239
|
+
def contents_performance(self) -> Any:
|
|
240
|
+
"""The numbers of the published cards, keyed by card id."""
|
|
241
|
+
return self._request("GET", "/public/v1/contents/performance")
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------ #
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def raw(self) -> httpx.Client:
|
|
247
|
+
"""The configured httpx client, for a call the facade does not wrap."""
|
|
248
|
+
return self._client
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def typed(self) -> Any:
|
|
252
|
+
"""The generated, fully typed client — every operation, with models.
|
|
253
|
+
|
|
254
|
+
>>> from usenadia.generated.api.public_api import list_channels
|
|
255
|
+
>>> list_channels.sync(client=nadia.typed)
|
|
256
|
+
|
|
257
|
+
Imported lazily so the facade still works from a checkout where the
|
|
258
|
+
client has not been generated yet (it is gitignored, see the README).
|
|
259
|
+
"""
|
|
260
|
+
if self._typed is None:
|
|
261
|
+
from .generated import AuthenticatedClient # noqa: PLC0415
|
|
262
|
+
|
|
263
|
+
self._typed = AuthenticatedClient(
|
|
264
|
+
base_url=str(self._client.base_url),
|
|
265
|
+
token=self._api_key,
|
|
266
|
+
# The API reads `Authorization: <key>`, with no `Bearer` prefix.
|
|
267
|
+
prefix="",
|
|
268
|
+
)
|
|
269
|
+
return self._typed
|
|
270
|
+
|
|
271
|
+
def close(self) -> None:
|
|
272
|
+
self._client.close()
|
|
273
|
+
|
|
274
|
+
def __enter__(self) -> "Nadia":
|
|
275
|
+
return self
|
|
276
|
+
|
|
277
|
+
def __exit__(self, *exc: Any) -> None:
|
|
278
|
+
self.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|