Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ff-171 simplifying rules creation #306

Merged
merged 73 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
20c9721
fill-db command
Tiendil Dec 17, 2024
83e8a33
todos
Tiendil Dec 17, 2024
931fa12
removed show tags filter
Tiendil Dec 17, 2024
b140f63
simplifying tags list
Tiendil Dec 17, 2024
be9950f
todos
Tiendil Dec 17, 2024
c4056cd
reload -> refresh
Tiendil Dec 17, 2024
d0f328f
something works, but bad
Tiendil Dec 17, 2024
efeced5
removed logging
Tiendil Dec 17, 2024
507829d
fixes
Tiendil Dec 17, 2024
9a8ab2d
changes tags selection/reverting
Tiendil Dec 17, 2024
d567652
reverse button moved to ffuntag.vue
Tiendil Dec 17, 2024
1b81611
cleanup
Tiendil Dec 17, 2024
a3631e7
code formatting
Tiendil Dec 17, 2024
a0ee913
types
Tiendil Dec 17, 2024
46cb026
fixes
Tiendil Dec 17, 2024
9aa5978
munor fixes for the rules view
Tiendil Dec 20, 2024
66b1c20
better filter text
Tiendil Dec 20, 2024
73e7a35
boilerplate for rule gui
Tiendil Dec 20, 2024
c2ed1e9
todo
Tiendil Dec 20, 2024
21cf46c
rules api change boilerplate
Tiendil Dec 20, 2024
173f12b
fixing tests
Tiendil Dec 20, 2024
229a4ef
removed prints
Tiendil Dec 20, 2024
b067a3a
operation tests fixes
Tiendil Dec 20, 2024
ee215c7
more tests
Tiendil Dec 20, 2024
0723f75
fixes
Tiendil Dec 20, 2024
6cf3a04
code formatting
Tiendil Dec 20, 2024
073f6b7
fixes
Tiendil Dec 20, 2024
24fe9cb
fixes
Tiendil Dec 20, 2024
5d8fae1
fixed new rules creation block layout
Tiendil Dec 21, 2024
af0080a
fixing api integration
Tiendil Dec 21, 2024
76ca099
fixing api integration
Tiendil Dec 21, 2024
d6b72e7
fixing rules view
Tiendil Dec 21, 2024
2ab35b5
updateRule api fixed
Tiendil Dec 21, 2024
262a0d1
fixes
Tiendil Dec 21, 2024
dd31107
success message
Tiendil Dec 21, 2024
7e59cbf
todo
Tiendil Dec 21, 2024
80bf77a
todo
Tiendil Dec 21, 2024
adf3e54
fixes
Tiendil Dec 21, 2024
9b9313b
fixes
Tiendil Dec 21, 2024
c82ffaf
fixing
Tiendil Dec 21, 2024
08838aa
open entry body by clicking on [x more] tags
Tiendil Dec 21, 2024
f009acc
todo
Tiendil Dec 21, 2024
6d51406
todo
Tiendil Dec 21, 2024
b63e44f
todos
Tiendil Dec 21, 2024
63a512f
formatting
Tiendil Dec 21, 2024
87954a0
fixes
Tiendil Dec 21, 2024
bcaf863
todo
Tiendil Dec 21, 2024
cbe5ef2
todos
Tiendil Dec 21, 2024
8bc0963
todo
Tiendil Dec 21, 2024
b26ab10
todo
Tiendil Dec 21, 2024
1108405
fix
Tiendil Dec 21, 2024
8d19584
fixed migration
Tiendil Dec 22, 2024
d361d4d
cli command to merge users
Tiendil Dec 22, 2024
b621129
fix
Tiendil Dec 22, 2024
a28ffa6
fix [more] displaying
Tiendil Dec 22, 2024
c027d3a
fixes
Tiendil Dec 22, 2024
433d4d4
improve code
Tiendil Dec 22, 2024
6b814d2
colorize tags on the rules view
Tiendil Dec 22, 2024
52d59fa
optimized news preparation
Tiendil Dec 22, 2024
ad47c20
todos
Tiendil Dec 22, 2024
69c20c4
single body show
Tiendil Dec 22, 2024
b86d9a7
todos
Tiendil Dec 22, 2024
397245c
spedup news preparation
Tiendil Dec 22, 2024
e81c551
tags filtering optimizations
Tiendil Dec 22, 2024
7ec318e
tags optimized
Tiendil Dec 22, 2024
1d4d7c4
fixed tags filter
Tiendil Dec 22, 2024
48420ee
fixed rules creation and filter
Tiendil Dec 22, 2024
79ec41b
fixed notifications
Tiendil Dec 22, 2024
2656613
code formatting
Tiendil Dec 22, 2024
e287348
fixing types
Tiendil Dec 22, 2024
e4832fe
fixes
Tiendil Dec 22, 2024
ac65b90
formatting
Tiendil Dec 22, 2024
cd555e2
changes
Tiendil Dec 22, 2024
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
8 changes: 8 additions & 0 deletions changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@

- ff-174 — Custom API entry points for OpenAI and Gemini clients.
- ff-175 — Export feeds to OPML file.
- ff-171 — Simplified rules creation:
- The rules creation form was moved to the tags filter.
- Rules now allow specifying excluded tags (will not apply if the news has any of them).
- Tags under the news caption now behave as tags in the tags filter.
- Button `[more]` on the tags line under the news caption now opens the whole news.
- Openned news items now always show all tags.
- Only a single news item now can be opened.
- Performance of the News page improved 2-3 times.
21 changes: 12 additions & 9 deletions ffun/ffun/api/entities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
import enum
import uuid
from decimal import Decimal
from typing import Any, Iterable

Expand All @@ -10,7 +9,7 @@
from ffun.api import front_events
from ffun.core import api
from ffun.core.entities import BaseEntity
from ffun.domain.entities import AbsoluteUrl, EntryId, FeedId, FeedUrl, UnknownUrl, UserId
from ffun.domain.entities import AbsoluteUrl, EntryId, FeedId, FeedUrl, RuleId, UnknownUrl, UserId
from ffun.feeds import entities as f_entities
from ffun.feeds_collections import entities as fc_entities
from ffun.feeds_links import entities as fl_entities
Expand Down Expand Up @@ -103,8 +102,9 @@ def from_internal( # noqa: CFQ002


class Rule(BaseEntity):
id: uuid.UUID
tags: list[str]
id: RuleId
requiredTags: list[str]
excludedTags: list[str]
score: int
createdAt: datetime.datetime
updatedAt: datetime.datetime
Expand All @@ -113,7 +113,8 @@ class Rule(BaseEntity):
def from_internal(cls, rule: s_entities.Rule, tags_mapping: dict[int, str]) -> "Rule":
return cls(
id=rule.id,
tags=[tags_mapping[tag_id] for tag_id in rule.tags],
requiredTags=[tags_mapping[tag_id] for tag_id in rule.required_tags],
excludedTags=[tags_mapping[tag_id] for tag_id in rule.excluded_tags],
score=rule.score,
createdAt=rule.created_at,
updatedAt=rule.updated_at,
Expand Down Expand Up @@ -319,7 +320,8 @@ class GetEntriesByIdsResponse(api.APISuccess):


class CreateOrUpdateRuleRequest(api.APIRequest):
tags: list[str]
requiredTags: list[str]
excludedTags: list[str]
score: int


Expand All @@ -328,16 +330,17 @@ class CreateOrUpdateRuleResponse(api.APISuccess):


class DeleteRuleRequest(api.APIRequest):
id: uuid.UUID
id: RuleId


class DeleteRuleResponse(api.APISuccess):
pass


class UpdateRuleRequest(api.APIRequest):
id: uuid.UUID
tags: list[str]
id: RuleId
requiredTags: list[str]
excludedTags: list[str]
score: int


Expand Down
28 changes: 21 additions & 7 deletions ffun/ffun/api/http_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,15 @@ async def api_get_entries_by_ids(
async def api_create_or_update_rule(
request: entities.CreateOrUpdateRuleRequest, user: User
) -> entities.CreateOrUpdateRuleResponse:
tags_ids = await o_domain.get_ids_by_uids(request.tags)

await s_domain.create_or_update_rule(user_id=user.id, tags=set(tags_ids.values()), score=request.score)
required_tags_ids = await o_domain.get_ids_by_uids(request.requiredTags)
excluded_tags_ids = await o_domain.get_ids_by_uids(request.excludedTags)

await s_domain.create_or_update_rule(
user_id=user.id,
score=request.score,
required_tags=required_tags_ids.values(),
excluded_tags=excluded_tags_ids.values(),
)

return entities.CreateOrUpdateRuleResponse()

Expand All @@ -164,9 +170,16 @@ async def api_delete_rule(request: entities.DeleteRuleRequest, user: User) -> en

@router.post("/api/update-rule")
async def api_update_rule(request: entities.UpdateRuleRequest, user: User) -> entities.UpdateRuleResponse:
tags_ids = await o_domain.get_ids_by_uids(request.tags)

await s_domain.update_rule(user_id=user.id, rule_id=request.id, score=request.score, tags=tags_ids.values())
required_tags_ids = await o_domain.get_ids_by_uids(request.requiredTags)
excluded_tags_ids = await o_domain.get_ids_by_uids(request.excludedTags)

await s_domain.update_rule(
user_id=user.id,
rule_id=request.id,
score=request.score,
required_tags=required_tags_ids.values(),
excluded_tags=excluded_tags_ids.values(),
)

return entities.UpdateRuleResponse()

Expand All @@ -175,7 +188,8 @@ async def _prepare_rules(rules: Iterable[s_entities.Rule]) -> list[entities.Rule
all_tags = set()

for rule in rules:
all_tags.update(rule.tags)
all_tags.update(rule.required_tags)
all_tags.update(rule.excluded_tags)

tags_mapping = await o_domain.get_tags_by_ids(all_tags)

Expand Down
2 changes: 2 additions & 0 deletions ffun/ffun/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ffun.cli.commands import cleaner # noqa: F401
from ffun.cli.commands import experiments # noqa: F401
from ffun.cli.commands import fixtures # noqa: F401
from ffun.cli.commands import metrics # noqa: F401
from ffun.cli.commands import processors_quality # noqa: F401
from ffun.cli.commands import profile # noqa: F401
Expand All @@ -19,6 +20,7 @@
app.add_typer(metrics.cli_app, name="metrics")
app.add_typer(profile.cli_app, name="profile")
app.add_typer(experiments.cli_app, name="experiments")
app.add_typer(fixtures.cli_app, name="fixtures")


if __name__ == "__main__":
Expand Down
142 changes: 142 additions & 0 deletions ffun/ffun/cli/commands/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
Development utilities for feeds.
This code is intended to be used by developers to simplify our work, therefore:
- It MUST NOT be used in production code.
- It MUST NOT be used in tests.
- no tests are required for this code.
"""

import asyncio
import uuid

import typer

from ffun.application.application import with_app
from ffun.auth.settings import settings as a_settings
from ffun.core import logging, utils
from ffun.domain.domain import new_entry_id, new_feed_id
from ffun.domain.entities import UnknownUrl, UserId
from ffun.domain.urls import adjust_classic_relative_url, str_to_feed_url, url_to_source_uid
from ffun.feeds.domain import get_feeds, get_source_ids, save_feed
from ffun.feeds.entities import Feed, FeedState
from ffun.feeds_links.domain import add_link
from ffun.library.domain import catalog_entries, get_entries_by_ids
from ffun.library.entities import Entry
from ffun.ontology.domain import apply_tags_to_entry
from ffun.ontology.entities import ProcessorTag
from ffun.users import domain as u_domain
from ffun.users import entities as u_entities

logger = logging.get_module_logger()

cli_app = typer.Typer()


async def fake_feed() -> Feed:

_id = uuid.uuid4().hex

url = str_to_feed_url(f"https://{_id}.com")

source_uid = url_to_source_uid(url)

source_ids = await get_source_ids([source_uid])

timestamp = utils.now()

feed = Feed(
id=new_feed_id(),
source_id=source_ids[source_uid],
url=url,
state=FeedState.loaded,
last_error=None,
load_attempted_at=timestamp,
loaded_at=timestamp,
title=f"Title {_id}",
description=f"Description {_id}",
)

feed_id = await save_feed(feed)

feeds = await get_feeds([feed_id])

return feeds[0]


async def fake_entry(feed: Feed) -> Entry:
_id = uuid.uuid4().hex

timestamp = utils.now()

url = adjust_classic_relative_url(UnknownUrl(f"enrty-{_id}"), feed.url)

assert url is not None

entry = Entry(
id=new_entry_id(),
source_id=feed.source_id,
title=f"Title {_id}",
body=f"Body {_id}",
external_id=uuid.uuid4().hex,
external_url=url,
external_tags=set(),
published_at=timestamp,
cataloged_at=timestamp,
)

await catalog_entries(feed.id, [entry])

entries = await get_entries_by_ids([entry.id])

assert entry is not None

returned_entry = entries[entry.id]

assert returned_entry is not None

return returned_entry


async def run_fill_db(feeds_number: int, entries_per_feed: int, tags_per_entry: int) -> None:
async with with_app():
external_user_id = a_settings.single_user.external_id

internal_user_id = await u_domain.get_or_create_user_id(u_entities.Service.single, external_user_id)

for _ in range(feeds_number):
feed = await fake_feed()

await add_link(internal_user_id, feed.id)

entries = [await fake_entry(feed) for _ in range(entries_per_feed)]

for i, entry in enumerate(entries, start=1):

tags = []

for j, _ in enumerate(range(tags_per_entry), start=1):
raw_uid = f"some-long-tag-name-{j}-{i % j}"
tags.append(ProcessorTag(raw_uid=raw_uid))

await apply_tags_to_entry(entry.id, 100500, tags)


@cli_app.command()
def fill_db(feeds_number: int = 10, entries_per_feed: int = 100, tags_per_entry: int = 25) -> None:
asyncio.run(run_fill_db(feeds_number, entries_per_feed, tags_per_entry))


async def run_supertokens_user_to_dev(intenal_user_id: UserId) -> None:
async with with_app():
external_user_id = a_settings.single_user.external_id

to_user_id = await u_domain.get_or_create_user_id(u_entities.Service.single, external_user_id)

await u_domain.tech_move_user(from_user_id=intenal_user_id, to_user_id=to_user_id)


@cli_app.command()
def supertokens_user_to_dev(intenal_user_id: str) -> None:
asyncio.run(run_supertokens_user_to_dev(UserId(uuid.UUID(intenal_user_id))))
6 changes: 5 additions & 1 deletion ffun/ffun/domain/domain.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid

from ffun.domain.entities import CollectionId, EntryId, FeedId, SourceId, UserId
from ffun.domain.entities import CollectionId, EntryId, FeedId, RuleId, SourceId, UserId


def new_user_id() -> UserId:
Expand All @@ -21,3 +21,7 @@ def new_collection_id() -> CollectionId:

def new_source_id() -> SourceId:
return SourceId(uuid.uuid4())


def new_rule_id() -> RuleId:
return RuleId(uuid.uuid4())
1 change: 1 addition & 0 deletions ffun/ffun/domain/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
FeedId = NewType("FeedId", uuid.UUID)
CollectionId = NewType("CollectionId", uuid.UUID)
SourceId = NewType("SourceId", uuid.UUID)
RuleId = NewType("RuleId", uuid.UUID)

# URL types for better normalization control in code
# conversion schemas:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,43 +83,26 @@
"""


# TODO: remove prints after migration applied to prod
def apply_step(conn: Connection[dict[str, Any]]) -> None:
cursor = conn.cursor(row_factory=dict_row)

print("Creating l_feeds_to_entries table") # noqa

cursor.execute(sql_create_feeds_to_entries_table)

print("Creating l_orphaned_entries table") # noqa

cursor.execute(sql_create_orphaned_entries_table)

print("Filling l_feeds_to_entries table") # noqa

cursor.execute(sql_fill_from_entries_table)

print("Filling l_orphaned_entries table") # noqa

cursor.execute(sql_fill_orphaned_entries)

cursor.execute("CREATE INDEX l_feeds_to_entries_entry_id_idx ON l_feeds_to_entries (entry_id)")

print("Filling duplicated entries") # noqa

cursor.execute(sql_fill_duplicated_entries)

print("Removing duplicated entries") # noqa

cursor.execute(sql_remove_duplicated_entries)

print("Creating unique index on l_entries") # noqa

# external_id goes first to optimize queries
cursor.execute("CREATE UNIQUE INDEX l_entries_source_id_external_id_idx ON l_entries (external_id, source_id)")

print("Removing feed_id column from l_entries") # noqa

cursor.execute("ALTER TABLE l_entries DROP COLUMN feed_id")

cursor.execute(
Expand All @@ -129,8 +112,6 @@ def apply_step(conn: Connection[dict[str, Any]]) -> None:

cursor.execute("CREATE INDEX l_entries_created_at_idx ON l_entries (created_at ASC)")

print("Completed") # noqa


def rollback_step(conn: Connection[dict[str, Any]]) -> None:
cursor = conn.cursor()
Expand Down
22 changes: 22 additions & 0 deletions ffun/ffun/ontology/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,25 @@ def three_tags_ids(three_tags_by_uids: dict[str, int]) -> tuple[int, int, int]:
@pytest.fixture
def three_processor_tags(three_tags_by_ids: dict[int, str]) -> tuple[ProcessorTag, ProcessorTag, ProcessorTag]:
return tuple(ProcessorTag(raw_uid=tag) for tag in three_tags_by_ids.values()) # type: ignore


@pytest_asyncio.fixture
async def five_tags_by_uids() -> dict[str, int]:
return await get_ids_by_uids([uuid.uuid4().hex for _ in range(5)])


@pytest.fixture
def five_tags_by_ids(five_tags_by_uids: dict[str, int]) -> dict[int, str]:
return {v: k for k, v in five_tags_by_uids.items()}


@pytest.fixture
def five_tags_ids(five_tags_by_uids: dict[str, int]) -> tuple[int, int, int, int, int]:
return tuple(five_tags_by_uids.values()) # type: ignore


@pytest.fixture
def five_processor_tags(
five_tags_by_ids: dict[int, str]
) -> tuple[ProcessorTag, ProcessorTag, ProcessorTag, ProcessorTag, ProcessorTag]:
return tuple(ProcessorTag(raw_uid=tag) for tag in five_tags_by_ids.values()) # type: ignore
Loading