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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,24 @@ All notable changes to `fopost` are documented here.

## Unreleased

### Removed

- **Breaking:** `BlogSettings.auto_publish` and the `auto_publish` argument of
`blog.update_settings` are gone. Nothing publishes without approval: a draft goes to
`in_review` through `blog.submit_article` first.

### Added

- `blog.suggest_seeds(workspace_id, seeds=None, prompt=None)` returns about 10 search terms
from the blog goal and topics (`ai` scope; an empty balance raises `PaymentRequiredError`).
- `blog.submit_article` moves a draft, failed or rejected article to `in_review` (`posts`
scope, 409 from any other state). `publish_article` and `publish_at` now need an article
in review, so a draft is a 409 until it is submitted.
- `BlogSettings` carries `goal`, `focus_topics`, `avoid_topics` and `plan_run` (a
`BlogPlanRun`), and `blog.update_settings` takes `goal`, `focus_topics` and `avoid_topics`.
`BlogKeyword.fit_reason`, `BLOG_GOALS`, and `in_review` in `BLOG_ARTICLE_STATUSES`.
`plan_keywords` no longer fills the calendar: setting a keyword to `planned` picks it, and
`review_hold_days` is how many days before its date it starts being written.
- `media.limits(workspace_id=None)` returns an `UploadLimits` model: `max_file_bytes`,
`multipart_max_file_bytes`, `max_files_per_upload`, `next_tier`, `allowed_types` and the
source of the ceiling.
Expand All @@ -20,6 +36,8 @@ All notable changes to `fopost` are documented here.
and `upgrade_url`.
- `client.blog`: settings, keyword planning and articles under `/v1/blog`, with the
`BlogSettings`, `BlogKeyword`, `BlogArticle`, `BlogArticleFull` and `BlogArticleImage` models.
The workspace travels as `workspace_id` on the query string; `test_website` returns the
site's `http_status`.
- `fopost.webhooks.verify` (also exported as `verify_webhook`): verifies any FoPost webhook
delivery over the raw body and returns the `{event, data, timestamp}` envelope, with the
typed `ArticlePublishedPayload` for `article.published`.
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ repurposed = client.ai.repurpose_url(
## Blog

`client.blog` plans keywords, writes articles and publishes them to a website. A post goes
to a social network; an article goes to a website.
to a social network; an article goes to a website. `suggest_seeds` proposes search terms from
the blog `goal` and topics, `plan_keywords` researches them into `suggested` keywords, and
setting a keyword's status to `planned` puts it on the calendar. A draft is submitted for
approval before it can be published.

```python
article = client.blog.request_article(
Expand All @@ -136,6 +139,7 @@ article = client.blog.request_article(
print(article.status) # "queued"; it becomes a draft once written

for draft in client.blog.list_articles(workspace.id, status="draft"):
client.blog.submit_article(workspace.id, draft.id)
client.blog.publish_article(workspace.id, draft.id)
```

Expand Down Expand Up @@ -324,7 +328,7 @@ except FopostError as err:
| `knowledge` | `list`, `create`, `update`, `delete`, `sync`, `search` |
| `ads` | `list`, `external`, `boostable`, `connections`, `sources`, `providers`, `authorize`, `delete_connection`, `boost`, `create`, `refresh`, `set_status`, `delete`, `audiences`, `create_audience`, `search_targeting`, `lead_forms`, `create_lead_form`, `leads`, `account_tree`, `create_campaign`, `get_campaign`, `update_campaign`, `delete_campaign`, `duplicate_campaign`, `create_ad_set`, `get_ad_set`, `update_ad_set`, `delete_ad_set`, `duplicate_ad_set`, `create_network_ad`, `get_network_ad`, `update_network_ad`, `delete_network_ad`, `duplicate_network_ad`, `bulk_set_status`, `creatives`, `create_creative`, `get_creative`, `delete_creative`, `get_audience`, `update_audience`, `delete_audience`, `add_audience_users`, `add_audience_companies`, `estimate_reach`, `insights`, `ad_insights`, `get_lead_form`, `archive_lead_form`, `leads_feed`, `lead_pages`, `subscribe_lead_page`, `unsubscribe_lead_page`, `bid_pricing`, `supply_forecast`, `conversion_rules`, `create_conversion_rule`, `get_conversion_rule`, `update_conversion_rule`, `delete_conversion_rule`, `attach_conversion_rule`, `detach_conversion_rule`, `conversion_metrics`, `send_conversion_events` |
| `google_business` | `get_location`, `update_location`, `get_attributes`, `update_attributes`, `get_menus`, `replace_menus`, `get_services`, `replace_services`, `list_media`, `add_media`, `delete_media`, `list_place_actions`, `create_place_action`, `update_place_action`, `delete_place_action`, `get_verification_options`, `start_verification`, `complete_verification`, `get_performance`, `get_search_keywords`, `assign` |
| `blog` | `get_settings`, `update_settings`, `rotate_website_secret`, `test_website`, `list_keywords`, `plan_keywords`, `add_keyword`, `update_keyword`, `delete_keyword`, `list_articles`, `request_article`, `get_article`, `update_article`, `regenerate_article`, `publish_article`, `reject_article`, `delete_article` |
| `blog` | `get_settings`, `update_settings`, `rotate_website_secret`, `test_website`, `list_keywords`, `suggest_seeds`, `plan_keywords`, `add_keyword`, `update_keyword`, `delete_keyword`, `list_articles`, `request_article`, `get_article`, `update_article`, `regenerate_article`, `submit_article`, `publish_article`, `reject_article`, `delete_article` |
| `validate` | `post`, `length`, `media` |
| `whatsapp` | `get_profile`, `update_profile`, `request_display_name`, `set_username`, `list_templates`, `list_template_library`, `get_template`, `create_template`, `import_template`, `update_template`, `delete_template`, `list_groups`, `create_group`, `get_group`, `update_group`, `delete_group`, `get_group_invite_link`, `reset_group_invite_link`, `remove_group_participants`, `list_blocked`, `block_users`, `unblock_users`, `get_commerce_settings`, `update_commerce_settings`, `link_catalog`, `list_flows`, `get_flow`, `create_flow`, `update_flow`, `delete_flow`, `upload_flow_json`, `publish_flow`, `deprecate_flow`, `list_flow_responses`, `get_encryption_key_status`, `set_encryption_key`, `get_account_events`, `list_sandbox_sessions`, `create_sandbox_session` |
| `analytics` | `decay`, `frequency`, `timeline`, `changes`, `collect_post`, `native_posts` |
Expand Down
6 changes: 6 additions & 0 deletions src/fopost/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from .models import (
BLOG_ARTICLE_STATUSES,
BLOG_GOALS,
BLOG_KEYWORD_STATUSES,
BLOG_TARGET_KINDS,
PLATFORMS,
Expand Down Expand Up @@ -71,8 +72,10 @@
BlogArticleFull,
BlogArticleImage,
BlogArticleStatus,
BlogGoal,
BlogKeyword,
BlogKeywordStatus,
BlogPlanRun,
BlogSettings,
BlogSettingsUpdate,
BlogTargetKind,
Expand Down Expand Up @@ -469,15 +472,18 @@
"ReachEstimate",
"SupplyForecast",
"BLOG_ARTICLE_STATUSES",
"BLOG_GOALS",
"BLOG_KEYWORD_STATUSES",
"BLOG_TARGET_KINDS",
"ArticlePublishedPayload",
"BlogArticle",
"BlogArticleFull",
"BlogArticleImage",
"BlogArticleStatus",
"BlogGoal",
"BlogKeyword",
"BlogKeywordStatus",
"BlogPlanRun",
"BlogSettings",
"BlogSettingsUpdate",
"BlogTargetKind",
Expand Down
36 changes: 30 additions & 6 deletions src/fopost/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2343,6 +2343,7 @@ class GoogleConversionAction(FopostModel):
"queued",
"writing",
"draft",
"in_review",
"scheduled",
"publishing",
"published",
Expand All @@ -2354,6 +2355,7 @@ class GoogleConversionAction(FopostModel):
"queued",
"writing",
"draft",
"in_review",
"scheduled",
"publishing",
"published",
Expand All @@ -2369,20 +2371,40 @@ class GoogleConversionAction(FopostModel):

BLOG_TARGET_KINDS: tuple[str, ...] = ("product_blog", "account", "website")

BlogGoal = Literal["traffic", "leads", "sales", "signups", "authority"]

BLOG_GOALS: tuple[str, ...] = ("traffic", "leads", "sales", "signups", "authority")


class BlogPlanRun(FopostModel):
"""The last keyword research run."""

#: ``queued``, ``running``, ``done`` or ``failed``.
status: str
error: str | None = None
#: New keywords the run found.
added: int | None = None
finished_at: datetime | None = None


class BlogSettings(FopostModel):
"""How a workspace plans, writes and publishes articles."""

site_url: str | None = None
topic: str | None = None
audience: str | None = None
#: One of ``BLOG_GOALS``; steers suggested terms and keyword ranking.
goal: str | None = None
#: What the blog wants to write about.
focus_topics: str | None = None
#: What the blog never writes about.
avoid_topics: str | None = None
language: str = "en"
location_code: int | None = None
#: Articles planned per week; 0 turns planning off.
cadence_per_week: int = 0
#: Days a draft waits for review before it may auto-publish.
#: Days before its planned date that a picked keyword starts being written.
review_hold_days: int = 0
auto_publish: bool = False
image_tier: str | None = None
in_body_images: int | None = None
min_words: int | None = None
Expand All @@ -2394,6 +2416,8 @@ class BlogSettings(FopostModel):
target_platform: str | None = None
website_url: str | None = None
has_website_secret: bool = False
#: ``None`` before the first run.
plan_run: BlogPlanRun | None = None
keyword_data_available: bool = False
updated_at: datetime | None = None

Expand All @@ -2406,11 +2430,9 @@ class BlogSettingsUpdate(FopostModel):


class BlogWebsiteTest(FopostModel):
"""Outcome of a test delivery to the website target."""
"""What the website answered a test delivery it accepted."""

ok: bool = False
status: int | None = None
error: str | None = None
http_status: int


class BlogKeyword(FopostModel):
Expand All @@ -2426,6 +2448,8 @@ class BlogKeyword(FopostModel):
#: ``suggested``, ``planned``, ``written`` or ``dismissed``.
status: str = "suggested"
planned_for: date | None = None
#: Why the keyword serves the blog goal.
fit_reason: str | None = None
created_at: datetime | None = None


Expand Down
Loading
Loading