mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
Removed friendly name support and tidied tags
This commit is contained in:
@@ -19,12 +19,12 @@ from meshcore_hub.common.schemas.messages import (
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _get_friendly_name(node: Optional[Node]) -> Optional[str]:
|
||||
"""Extract friendly_name tag from a node's tags."""
|
||||
def _get_tag_name(node: Optional[Node]) -> Optional[str]:
|
||||
"""Extract name tag from a node's tags."""
|
||||
if not node or not node.tags:
|
||||
return None
|
||||
for tag in node.tags:
|
||||
if tag.key == "friendly_name":
|
||||
if tag.key == "name":
|
||||
return tag.value
|
||||
return None
|
||||
|
||||
@@ -57,15 +57,15 @@ def _fetch_receivers_for_events(
|
||||
receivers_by_hash: dict[str, list[ReceiverInfo]] = {}
|
||||
|
||||
node_ids = [r.node_id for r in results]
|
||||
friendly_names: dict[str, str] = {}
|
||||
tag_names: dict[str, str] = {}
|
||||
if node_ids:
|
||||
fn_query = (
|
||||
tag_query = (
|
||||
select(NodeTag.node_id, NodeTag.value)
|
||||
.where(NodeTag.node_id.in_(node_ids))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
.where(NodeTag.key == "name")
|
||||
)
|
||||
for node_id, value in session.execute(fn_query).all():
|
||||
friendly_names[node_id] = value
|
||||
for node_id, value in session.execute(tag_query).all():
|
||||
tag_names[node_id] = value
|
||||
|
||||
for row in results:
|
||||
if row.event_hash not in receivers_by_hash:
|
||||
@@ -76,7 +76,7 @@ def _fetch_receivers_for_events(
|
||||
node_id=row.node_id,
|
||||
public_key=row.public_key,
|
||||
name=row.name,
|
||||
friendly_name=friendly_names.get(row.node_id),
|
||||
tag_name=tag_names.get(row.node_id),
|
||||
snr=row.snr,
|
||||
received_at=row.received_at,
|
||||
)
|
||||
@@ -173,11 +173,11 @@ async def list_advertisements(
|
||||
data = {
|
||||
"received_by": row.receiver_pk,
|
||||
"receiver_name": row.receiver_name,
|
||||
"receiver_friendly_name": _get_friendly_name(receiver_node),
|
||||
"receiver_tag_name": _get_tag_name(receiver_node),
|
||||
"public_key": adv.public_key,
|
||||
"name": adv.name,
|
||||
"node_name": row.source_name,
|
||||
"node_friendly_name": _get_friendly_name(source_node),
|
||||
"node_tag_name": _get_tag_name(source_node),
|
||||
"adv_type": adv.adv_type or row.source_adv_type,
|
||||
"flags": adv.flags,
|
||||
"received_at": adv.received_at,
|
||||
@@ -255,11 +255,11 @@ async def get_advertisement(
|
||||
data = {
|
||||
"received_by": result.receiver_pk,
|
||||
"receiver_name": result.receiver_name,
|
||||
"receiver_friendly_name": _get_friendly_name(receiver_node),
|
||||
"receiver_tag_name": _get_tag_name(receiver_node),
|
||||
"public_key": adv.public_key,
|
||||
"name": adv.name,
|
||||
"node_name": result.source_name,
|
||||
"node_friendly_name": _get_friendly_name(source_node),
|
||||
"node_tag_name": _get_tag_name(source_node),
|
||||
"adv_type": adv.adv_type or result.source_adv_type,
|
||||
"flags": adv.flags,
|
||||
"received_at": adv.received_at,
|
||||
|
||||
@@ -82,11 +82,11 @@ async def get_stats(
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get node names, adv_types, and friendly_name tags for the advertised nodes
|
||||
# Get node names, adv_types, and name tags for the advertised nodes
|
||||
ad_public_keys = [ad.public_key for ad in recent_ads]
|
||||
node_names: dict[str, str] = {}
|
||||
node_adv_types: dict[str, str] = {}
|
||||
friendly_names: dict[str, str] = {}
|
||||
tag_names: dict[str, str] = {}
|
||||
if ad_public_keys:
|
||||
# Get node names and adv_types from Node table
|
||||
node_query = select(Node.public_key, Node.name, Node.adv_type).where(
|
||||
@@ -98,21 +98,21 @@ async def get_stats(
|
||||
if adv_type:
|
||||
node_adv_types[public_key] = adv_type
|
||||
|
||||
# Get friendly_name tags
|
||||
friendly_name_query = (
|
||||
# Get name tags
|
||||
tag_name_query = (
|
||||
select(Node.public_key, NodeTag.value)
|
||||
.join(NodeTag, Node.id == NodeTag.node_id)
|
||||
.where(Node.public_key.in_(ad_public_keys))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
.where(NodeTag.key == "name")
|
||||
)
|
||||
for public_key, value in session.execute(friendly_name_query).all():
|
||||
friendly_names[public_key] = value
|
||||
for public_key, value in session.execute(tag_name_query).all():
|
||||
tag_names[public_key] = value
|
||||
|
||||
recent_advertisements = [
|
||||
RecentAdvertisement(
|
||||
public_key=ad.public_key,
|
||||
name=ad.name or node_names.get(ad.public_key),
|
||||
friendly_name=friendly_names.get(ad.public_key),
|
||||
tag_name=tag_names.get(ad.public_key),
|
||||
adv_type=ad.adv_type or node_adv_types.get(ad.public_key),
|
||||
received_at=ad.received_at,
|
||||
)
|
||||
@@ -146,7 +146,7 @@ async def get_stats(
|
||||
# Look up sender names for these messages
|
||||
msg_prefixes = [m.pubkey_prefix for m in channel_msgs if m.pubkey_prefix]
|
||||
msg_sender_names: dict[str, str] = {}
|
||||
msg_friendly_names: dict[str, str] = {}
|
||||
msg_tag_names: dict[str, str] = {}
|
||||
if msg_prefixes:
|
||||
for prefix in set(msg_prefixes):
|
||||
sender_node_query = select(Node.public_key, Node.name).where(
|
||||
@@ -156,14 +156,14 @@ async def get_stats(
|
||||
if name:
|
||||
msg_sender_names[public_key[:12]] = name
|
||||
|
||||
sender_friendly_query = (
|
||||
sender_tag_query = (
|
||||
select(Node.public_key, NodeTag.value)
|
||||
.join(NodeTag, Node.id == NodeTag.node_id)
|
||||
.where(Node.public_key.startswith(prefix))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
.where(NodeTag.key == "name")
|
||||
)
|
||||
for public_key, value in session.execute(sender_friendly_query).all():
|
||||
msg_friendly_names[public_key[:12]] = value
|
||||
for public_key, value in session.execute(sender_tag_query).all():
|
||||
msg_tag_names[public_key[:12]] = value
|
||||
|
||||
channel_messages[int(channel_idx)] = [
|
||||
ChannelMessage(
|
||||
@@ -171,8 +171,8 @@ async def get_stats(
|
||||
sender_name=(
|
||||
msg_sender_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
sender_friendly_name=(
|
||||
msg_friendly_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
sender_tag_name=(
|
||||
msg_tag_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
pubkey_prefix=m.pubkey_prefix,
|
||||
received_at=m.received_at,
|
||||
|
||||
@@ -41,7 +41,7 @@ def _enrich_member_nodes(
|
||||
updated_at=mn.updated_at,
|
||||
node_name=info.get("name"),
|
||||
node_adv_type=info.get("adv_type"),
|
||||
friendly_name=info.get("friendly_name"),
|
||||
tag_name=info.get("tag_name"),
|
||||
)
|
||||
)
|
||||
return enriched_nodes
|
||||
@@ -100,15 +100,15 @@ async def list_members(
|
||||
)
|
||||
nodes = session.execute(node_query).scalars().all()
|
||||
for node in nodes:
|
||||
friendly_name = None
|
||||
tag_name = None
|
||||
for tag in node.tags:
|
||||
if tag.key == "friendly_name":
|
||||
friendly_name = tag.value
|
||||
if tag.key == "name":
|
||||
tag_name = tag.value
|
||||
break
|
||||
node_info[node.public_key] = {
|
||||
"name": node.name,
|
||||
"adv_type": node.adv_type,
|
||||
"friendly_name": friendly_name,
|
||||
"tag_name": tag_name,
|
||||
}
|
||||
|
||||
return MemberList(
|
||||
@@ -145,15 +145,15 @@ async def get_member(
|
||||
)
|
||||
nodes = session.execute(node_query).scalars().all()
|
||||
for node in nodes:
|
||||
friendly_name = None
|
||||
tag_name = None
|
||||
for tag in node.tags:
|
||||
if tag.key == "friendly_name":
|
||||
friendly_name = tag.value
|
||||
if tag.key == "name":
|
||||
tag_name = tag.value
|
||||
break
|
||||
node_info[node.public_key] = {
|
||||
"name": node.name,
|
||||
"adv_type": node.adv_type,
|
||||
"friendly_name": friendly_name,
|
||||
"tag_name": tag_name,
|
||||
}
|
||||
|
||||
return _member_to_read(member, node_info)
|
||||
|
||||
@@ -15,12 +15,12 @@ from meshcore_hub.common.schemas.messages import MessageList, MessageRead, Recei
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _get_friendly_name(node: Optional[Node]) -> Optional[str]:
|
||||
"""Extract friendly_name tag from a node's tags."""
|
||||
def _get_tag_name(node: Optional[Node]) -> Optional[str]:
|
||||
"""Extract name tag from a node's tags."""
|
||||
if not node or not node.tags:
|
||||
return None
|
||||
for tag in node.tags:
|
||||
if tag.key == "friendly_name":
|
||||
if tag.key == "name":
|
||||
return tag.value
|
||||
return None
|
||||
|
||||
@@ -64,17 +64,17 @@ def _fetch_receivers_for_events(
|
||||
# Group by event_hash
|
||||
receivers_by_hash: dict[str, list[ReceiverInfo]] = {}
|
||||
|
||||
# Get friendly names for receiver nodes
|
||||
# Get tag names for receiver nodes
|
||||
node_ids = [r.node_id for r in results]
|
||||
friendly_names: dict[str, str] = {}
|
||||
tag_names: dict[str, str] = {}
|
||||
if node_ids:
|
||||
fn_query = (
|
||||
tag_query = (
|
||||
select(NodeTag.node_id, NodeTag.value)
|
||||
.where(NodeTag.node_id.in_(node_ids))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
.where(NodeTag.key == "name")
|
||||
)
|
||||
for node_id, value in session.execute(fn_query).all():
|
||||
friendly_names[node_id] = value
|
||||
for node_id, value in session.execute(tag_query).all():
|
||||
tag_names[node_id] = value
|
||||
|
||||
for row in results:
|
||||
if row.event_hash not in receivers_by_hash:
|
||||
@@ -85,7 +85,7 @@ def _fetch_receivers_for_events(
|
||||
node_id=row.node_id,
|
||||
public_key=row.public_key,
|
||||
name=row.name,
|
||||
friendly_name=friendly_names.get(row.node_id),
|
||||
tag_name=tag_names.get(row.node_id),
|
||||
snr=row.snr,
|
||||
received_at=row.received_at,
|
||||
)
|
||||
@@ -153,10 +153,10 @@ async def list_messages(
|
||||
# Execute
|
||||
results = session.execute(query).all()
|
||||
|
||||
# Look up sender names and friendly_names for senders with pubkey_prefix
|
||||
# Look up sender names and tag names for senders with pubkey_prefix
|
||||
pubkey_prefixes = [r[0].pubkey_prefix for r in results if r[0].pubkey_prefix]
|
||||
sender_names: dict[str, str] = {}
|
||||
friendly_names: dict[str, str] = {}
|
||||
sender_tag_names: dict[str, str] = {}
|
||||
if pubkey_prefixes:
|
||||
# Find nodes whose public_key starts with any of these prefixes
|
||||
for prefix in set(pubkey_prefixes):
|
||||
@@ -168,15 +168,15 @@ async def list_messages(
|
||||
if name:
|
||||
sender_names[public_key[:12]] = name
|
||||
|
||||
# Get friendly_name tag
|
||||
friendly_name_query = (
|
||||
# Get name tag
|
||||
tag_name_query = (
|
||||
select(Node.public_key, NodeTag.value)
|
||||
.join(NodeTag, Node.id == NodeTag.node_id)
|
||||
.where(Node.public_key.startswith(prefix))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
.where(NodeTag.key == "name")
|
||||
)
|
||||
for public_key, value in session.execute(friendly_name_query).all():
|
||||
friendly_names[public_key[:12]] = value
|
||||
for public_key, value in session.execute(tag_name_query).all():
|
||||
sender_tag_names[public_key[:12]] = value
|
||||
|
||||
# Collect receiver node IDs to fetch tags
|
||||
receiver_ids = set()
|
||||
@@ -214,14 +214,14 @@ async def list_messages(
|
||||
"receiver_node_id": m.receiver_node_id,
|
||||
"received_by": receiver_pk,
|
||||
"receiver_name": receiver_name,
|
||||
"receiver_friendly_name": _get_friendly_name(receiver_node),
|
||||
"receiver_tag_name": _get_tag_name(receiver_node),
|
||||
"message_type": m.message_type,
|
||||
"pubkey_prefix": m.pubkey_prefix,
|
||||
"sender_name": (
|
||||
sender_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
"sender_friendly_name": (
|
||||
friendly_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
"sender_tag_name": (
|
||||
sender_tag_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
"channel_idx": m.channel_idx,
|
||||
"text": m.text,
|
||||
|
||||
@@ -383,8 +383,11 @@ def _run_seed_import(
|
||||
file_path=str(node_tags_file),
|
||||
db=db,
|
||||
create_nodes=create_nodes,
|
||||
clear_existing=True,
|
||||
)
|
||||
if verbose:
|
||||
if stats["deleted"]:
|
||||
click.echo(f" Deleted {stats['deleted']} existing tags")
|
||||
click.echo(
|
||||
f" Tags: {stats['created']} created, {stats['updated']} updated"
|
||||
)
|
||||
@@ -428,16 +431,24 @@ def _run_seed_import(
|
||||
default=False,
|
||||
help="Skip tags for nodes that don't exist (default: create nodes)",
|
||||
)
|
||||
@click.option(
|
||||
"--clear-existing",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Delete all existing tags before importing",
|
||||
)
|
||||
@click.pass_context
|
||||
def import_tags_cmd(
|
||||
ctx: click.Context,
|
||||
file: str | None,
|
||||
no_create_nodes: bool,
|
||||
clear_existing: bool,
|
||||
) -> None:
|
||||
"""Import node tags from a YAML file.
|
||||
|
||||
Reads a YAML file containing tag definitions and upserts them
|
||||
into the database. Existing tags are updated, new tags are created.
|
||||
into the database. By default, existing tags are updated and new tags are created.
|
||||
Use --clear-existing to delete all tags before importing.
|
||||
|
||||
FILE is the path to the YAML file containing tags.
|
||||
If not provided, defaults to {SEED_HOME}/node_tags.yaml.
|
||||
@@ -492,11 +503,14 @@ def import_tags_cmd(
|
||||
file_path=tags_file,
|
||||
db=db,
|
||||
create_nodes=not no_create_nodes,
|
||||
clear_existing=clear_existing,
|
||||
)
|
||||
|
||||
# Report results
|
||||
click.echo("")
|
||||
click.echo("Import complete:")
|
||||
if stats["deleted"]:
|
||||
click.echo(f" Tags deleted: {stats['deleted']}")
|
||||
click.echo(f" Total tags in file: {stats['total']}")
|
||||
click.echo(f" Tags created: {stats['created']}")
|
||||
click.echo(f" Tags updated: {stats['updated']}")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import delete, func, select
|
||||
|
||||
from meshcore_hub.common.database import DatabaseManager
|
||||
from meshcore_hub.common.models import Node, NodeTag
|
||||
@@ -151,16 +151,19 @@ def import_tags(
|
||||
file_path: str | Path,
|
||||
db: DatabaseManager,
|
||||
create_nodes: bool = True,
|
||||
clear_existing: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Import tags from a YAML file into the database.
|
||||
|
||||
Performs upsert operations - existing tags are updated, new tags are created.
|
||||
Optionally clears all existing tags before import.
|
||||
|
||||
Args:
|
||||
file_path: Path to the tags YAML file
|
||||
db: Database manager instance
|
||||
create_nodes: If True, create nodes that don't exist. If False, skip tags
|
||||
for non-existent nodes.
|
||||
clear_existing: If True, delete all existing tags before importing.
|
||||
|
||||
Returns:
|
||||
Dictionary with import statistics:
|
||||
@@ -169,6 +172,7 @@ def import_tags(
|
||||
- updated: Number of existing tags updated
|
||||
- skipped: Number of tags skipped (node not found and create_nodes=False)
|
||||
- nodes_created: Number of new nodes created
|
||||
- deleted: Number of existing tags deleted (if clear_existing=True)
|
||||
- errors: List of error messages
|
||||
"""
|
||||
stats: dict[str, Any] = {
|
||||
@@ -177,6 +181,7 @@ def import_tags(
|
||||
"updated": 0,
|
||||
"skipped": 0,
|
||||
"nodes_created": 0,
|
||||
"deleted": 0,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
@@ -194,6 +199,15 @@ def import_tags(
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
with db.session_scope() as session:
|
||||
# Clear all existing tags if requested
|
||||
if clear_existing:
|
||||
delete_count = (
|
||||
session.execute(select(func.count()).select_from(NodeTag)).scalar() or 0
|
||||
)
|
||||
session.execute(delete(NodeTag))
|
||||
stats["deleted"] = delete_count
|
||||
logger.info(f"Deleted {delete_count} existing tags")
|
||||
|
||||
# Cache nodes by public_key to reduce queries
|
||||
node_cache: dict[str, Node] = {}
|
||||
|
||||
@@ -232,24 +246,8 @@ def import_tags(
|
||||
tag_value = tag_data.get("value")
|
||||
tag_type = tag_data.get("type", "string")
|
||||
|
||||
# Find or create tag
|
||||
tag_query = select(NodeTag).where(
|
||||
NodeTag.node_id == node.id,
|
||||
NodeTag.key == tag_key,
|
||||
)
|
||||
existing_tag = session.execute(tag_query).scalar_one_or_none()
|
||||
|
||||
if existing_tag:
|
||||
# Update existing tag
|
||||
existing_tag.value = tag_value
|
||||
existing_tag.value_type = tag_type
|
||||
stats["updated"] += 1
|
||||
logger.debug(
|
||||
f"Updated tag {tag_key}={tag_value} "
|
||||
f"for {public_key[:12]}..."
|
||||
)
|
||||
else:
|
||||
# Create new tag
|
||||
if clear_existing:
|
||||
# When clearing, always create new tags
|
||||
new_tag = NodeTag(
|
||||
node_id=node.id,
|
||||
key=tag_key,
|
||||
@@ -262,6 +260,39 @@ def import_tags(
|
||||
f"Created tag {tag_key}={tag_value} "
|
||||
f"for {public_key[:12]}..."
|
||||
)
|
||||
else:
|
||||
# Find or create tag
|
||||
tag_query = select(NodeTag).where(
|
||||
NodeTag.node_id == node.id,
|
||||
NodeTag.key == tag_key,
|
||||
)
|
||||
existing_tag = session.execute(
|
||||
tag_query
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing_tag:
|
||||
# Update existing tag
|
||||
existing_tag.value = tag_value
|
||||
existing_tag.value_type = tag_type
|
||||
stats["updated"] += 1
|
||||
logger.debug(
|
||||
f"Updated tag {tag_key}={tag_value} "
|
||||
f"for {public_key[:12]}..."
|
||||
)
|
||||
else:
|
||||
# Create new tag
|
||||
new_tag = NodeTag(
|
||||
node_id=node.id,
|
||||
key=tag_key,
|
||||
value=tag_value,
|
||||
value_type=tag_type,
|
||||
)
|
||||
session.add(new_tag)
|
||||
stats["created"] += 1
|
||||
logger.debug(
|
||||
f"Created tag {tag_key}={tag_value} "
|
||||
f"for {public_key[:12]}..."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error processing tag {tag_key} for {public_key[:12]}...: {e}"
|
||||
|
||||
@@ -35,9 +35,7 @@ class MemberNodeRead(BaseModel):
|
||||
node_adv_type: Optional[str] = Field(
|
||||
default=None, description="Node's advertisement type"
|
||||
)
|
||||
friendly_name: Optional[str] = Field(
|
||||
default=None, description="Node's friendly name tag"
|
||||
)
|
||||
tag_name: Optional[str] = Field(default=None, description="Node's name tag")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -12,9 +12,7 @@ class ReceiverInfo(BaseModel):
|
||||
node_id: str = Field(..., description="Receiver node UUID")
|
||||
public_key: str = Field(..., description="Receiver node public key")
|
||||
name: Optional[str] = Field(default=None, description="Receiver node name")
|
||||
friendly_name: Optional[str] = Field(
|
||||
default=None, description="Receiver friendly name from tags"
|
||||
)
|
||||
tag_name: Optional[str] = Field(default=None, description="Receiver name from tags")
|
||||
snr: Optional[float] = Field(
|
||||
default=None, description="Signal-to-noise ratio at this receiver"
|
||||
)
|
||||
@@ -31,8 +29,8 @@ class MessageRead(BaseModel):
|
||||
default=None, description="Receiving interface node public key"
|
||||
)
|
||||
receiver_name: Optional[str] = Field(default=None, description="Receiver node name")
|
||||
receiver_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Receiver friendly name from tags"
|
||||
receiver_tag_name: Optional[str] = Field(
|
||||
default=None, description="Receiver name from tags"
|
||||
)
|
||||
message_type: str = Field(..., description="Message type (contact, channel)")
|
||||
pubkey_prefix: Optional[str] = Field(
|
||||
@@ -41,8 +39,8 @@ class MessageRead(BaseModel):
|
||||
sender_name: Optional[str] = Field(
|
||||
default=None, description="Sender's advertised node name"
|
||||
)
|
||||
sender_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Sender's friendly name from node tags"
|
||||
sender_tag_name: Optional[str] = Field(
|
||||
default=None, description="Sender's name from node tags"
|
||||
)
|
||||
channel_idx: Optional[int] = Field(default=None, description="Channel index")
|
||||
text: str = Field(..., description="Message content")
|
||||
@@ -110,16 +108,16 @@ class AdvertisementRead(BaseModel):
|
||||
default=None, description="Receiving interface node public key"
|
||||
)
|
||||
receiver_name: Optional[str] = Field(default=None, description="Receiver node name")
|
||||
receiver_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Receiver friendly name from tags"
|
||||
receiver_tag_name: Optional[str] = Field(
|
||||
default=None, description="Receiver name from tags"
|
||||
)
|
||||
public_key: str = Field(..., description="Advertised public key")
|
||||
name: Optional[str] = Field(default=None, description="Advertised name")
|
||||
node_name: Optional[str] = Field(
|
||||
default=None, description="Node name from nodes table"
|
||||
)
|
||||
node_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Node friendly name from tags"
|
||||
node_tag_name: Optional[str] = Field(
|
||||
default=None, description="Node name from tags"
|
||||
)
|
||||
adv_type: Optional[str] = Field(default=None, description="Node type")
|
||||
flags: Optional[int] = Field(default=None, description="Capability flags")
|
||||
@@ -215,7 +213,7 @@ class RecentAdvertisement(BaseModel):
|
||||
|
||||
public_key: str = Field(..., description="Node public key")
|
||||
name: Optional[str] = Field(default=None, description="Node name")
|
||||
friendly_name: Optional[str] = Field(default=None, description="Friendly name tag")
|
||||
tag_name: Optional[str] = Field(default=None, description="Name tag")
|
||||
adv_type: Optional[str] = Field(default=None, description="Node type")
|
||||
received_at: datetime = Field(..., description="When received")
|
||||
|
||||
@@ -225,8 +223,8 @@ class ChannelMessage(BaseModel):
|
||||
|
||||
text: str = Field(..., description="Message text")
|
||||
sender_name: Optional[str] = Field(default=None, description="Sender name")
|
||||
sender_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Sender friendly name"
|
||||
sender_tag_name: Optional[str] = Field(
|
||||
default=None, description="Sender name from tags"
|
||||
)
|
||||
pubkey_prefix: Optional[str] = Field(
|
||||
default=None, description="Sender public key prefix"
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<a href="/nodes/{{ ad.public_key }}" class="link link-hover">
|
||||
{% if ad.node_friendly_name or ad.node_name or ad.name %}
|
||||
<div class="font-medium">{{ ad.node_friendly_name or ad.node_name or ad.name }}</div>
|
||||
{% if ad.node_tag_name or ad.node_name or ad.name %}
|
||||
<div class="font-medium">{{ ad.node_tag_name or ad.node_name or ad.name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ ad.public_key[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ ad.public_key[:16] }}...</span>
|
||||
@@ -80,7 +80,7 @@
|
||||
{% for recv in ad.receivers %}
|
||||
<li>
|
||||
<a href="/nodes/{{ recv.public_key }}" class="text-sm">
|
||||
{{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }}
|
||||
{{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -88,8 +88,8 @@
|
||||
</div>
|
||||
{% elif ad.receivers and ad.receivers|length == 1 %}
|
||||
<a href="/nodes/{{ ad.receivers[0].public_key }}" class="link link-hover">
|
||||
{% if ad.receivers[0].friendly_name or ad.receivers[0].name %}
|
||||
<div class="font-medium">{{ ad.receivers[0].friendly_name or ad.receivers[0].name }}</div>
|
||||
{% if ad.receivers[0].tag_name or ad.receivers[0].name %}
|
||||
<div class="font-medium">{{ ad.receivers[0].tag_name or ad.receivers[0].name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ ad.receivers[0].public_key[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ ad.receivers[0].public_key[:16] }}...</span>
|
||||
@@ -97,8 +97,8 @@
|
||||
</a>
|
||||
{% elif ad.received_by %}
|
||||
<a href="/nodes/{{ ad.received_by }}" class="link link-hover">
|
||||
{% if ad.receiver_friendly_name or ad.receiver_name %}
|
||||
<div class="font-medium">{{ ad.receiver_friendly_name or ad.receiver_name }}</div>
|
||||
{% if ad.receiver_tag_name or ad.receiver_name %}
|
||||
<div class="font-medium">{{ ad.receiver_tag_name or ad.receiver_name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ ad.received_by[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ ad.received_by[:16] }}...</span>
|
||||
|
||||
@@ -78,8 +78,8 @@
|
||||
{% if msg.message_type == 'channel' %}
|
||||
<span class="font-mono">CH{{ msg.channel_idx }}</span>
|
||||
{% else %}
|
||||
{% if msg.sender_friendly_name or msg.sender_name %}
|
||||
<span class="font-medium">{{ msg.sender_friendly_name or msg.sender_name }}</span>
|
||||
{% if msg.sender_tag_name or msg.sender_name %}
|
||||
<span class="font-medium">{{ msg.sender_tag_name or msg.sender_name }}</span>
|
||||
{% else %}
|
||||
<span class="font-mono text-xs">{{ (msg.pubkey_prefix or '-')[:12] }}</span>
|
||||
{% endif %}
|
||||
@@ -96,7 +96,7 @@
|
||||
{% for recv in msg.receivers %}
|
||||
<li>
|
||||
<a href="/nodes/{{ recv.public_key }}" class="text-sm">
|
||||
<span class="flex-1">{{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }}</span>
|
||||
<span class="flex-1">{{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }}</span>
|
||||
{% if recv.snr is not none %}
|
||||
<span class="badge badge-ghost badge-xs">{{ "%.1f"|format(recv.snr) }}</span>
|
||||
{% endif %}
|
||||
@@ -107,8 +107,8 @@
|
||||
</div>
|
||||
{% elif msg.receivers and msg.receivers|length == 1 %}
|
||||
<a href="/nodes/{{ msg.receivers[0].public_key }}" class="link link-hover">
|
||||
{% if msg.receivers[0].friendly_name or msg.receivers[0].name %}
|
||||
<div class="font-medium">{{ msg.receivers[0].friendly_name or msg.receivers[0].name }}</div>
|
||||
{% if msg.receivers[0].tag_name or msg.receivers[0].name %}
|
||||
<div class="font-medium">{{ msg.receivers[0].tag_name or msg.receivers[0].name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ msg.receivers[0].public_key[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ msg.receivers[0].public_key[:16] }}...</span>
|
||||
@@ -116,8 +116,8 @@
|
||||
</a>
|
||||
{% elif msg.received_by %}
|
||||
<a href="/nodes/{{ msg.received_by }}" class="link link-hover">
|
||||
{% if msg.receiver_friendly_name or msg.receiver_name %}
|
||||
<div class="font-medium">{{ msg.receiver_friendly_name or msg.receiver_name }}</div>
|
||||
{% if msg.receiver_tag_name or msg.receiver_name %}
|
||||
<div class="font-medium">{{ msg.receiver_tag_name or msg.receiver_name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ msg.received_by[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ msg.received_by[:16] }}...</span>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/nodes">Nodes</a></li>
|
||||
{% if node %}
|
||||
{% set ns = namespace(friendly_name=none) %}
|
||||
{% set ns = namespace(tag_name=none) %}
|
||||
{% for tag in node.tags or [] %}
|
||||
{% if tag.key == 'friendly_name' %}
|
||||
{% set ns.friendly_name = tag.value %}
|
||||
{% if tag.key == 'name' %}
|
||||
{% set ns.tag_name = tag.value %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>{{ ns.friendly_name or node.name or public_key[:12] + '...' }}</li>
|
||||
<li>{{ ns.tag_name or node.name or public_key[:12] + '...' }}</li>
|
||||
{% else %}
|
||||
<li>Not Found</li>
|
||||
{% endif %}
|
||||
@@ -31,17 +31,17 @@
|
||||
{% endif %}
|
||||
|
||||
{% if node %}
|
||||
{% set ns = namespace(friendly_name=none) %}
|
||||
{% set ns = namespace(tag_name=none) %}
|
||||
{% for tag in node.tags or [] %}
|
||||
{% if tag.key == 'friendly_name' %}
|
||||
{% set ns.friendly_name = tag.value %}
|
||||
{% if tag.key == 'name' %}
|
||||
{% set ns.tag_name = tag.value %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- Node Info Card -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title text-2xl">
|
||||
{{ ns.friendly_name or node.name or 'Unnamed Node' }}
|
||||
{{ ns.tag_name or node.name or 'Unnamed Node' }}
|
||||
{% if node.adv_type %}
|
||||
<span class="badge badge-secondary">{{ node.adv_type }}</span>
|
||||
{% endif %}
|
||||
@@ -125,8 +125,8 @@
|
||||
<td>
|
||||
{% if adv.received_by %}
|
||||
<a href="/nodes/{{ adv.received_by }}" class="link link-hover">
|
||||
{% if adv.receiver_friendly_name or adv.receiver_name %}
|
||||
<div class="font-medium text-sm">{{ adv.receiver_friendly_name or adv.receiver_name }}</div>
|
||||
{% if adv.receiver_tag_name or adv.receiver_name %}
|
||||
<div class="font-medium text-sm">{{ adv.receiver_tag_name or adv.receiver_name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ adv.received_by[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-xs">{{ adv.received_by[:16] }}...</span>
|
||||
@@ -175,8 +175,8 @@
|
||||
<td>
|
||||
{% if tel.received_by %}
|
||||
<a href="/nodes/{{ tel.received_by }}" class="link link-hover">
|
||||
{% if tel.receiver_friendly_name or tel.receiver_name %}
|
||||
<div class="font-medium text-sm">{{ tel.receiver_friendly_name or tel.receiver_name }}</div>
|
||||
{% if tel.receiver_tag_name or tel.receiver_name %}
|
||||
<div class="font-medium text-sm">{{ tel.receiver_tag_name or tel.receiver_name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ tel.received_by[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-xs">{{ tel.received_by[:16] }}...</span>
|
||||
|
||||
@@ -57,17 +57,17 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for node in nodes %}
|
||||
{% set ns = namespace(friendly_name=none) %}
|
||||
{% set ns = namespace(tag_name=none) %}
|
||||
{% for tag in node.tags or [] %}
|
||||
{% if tag.key == 'friendly_name' %}
|
||||
{% set ns.friendly_name = tag.value %}
|
||||
{% if tag.key == 'name' %}
|
||||
{% set ns.tag_name = tag.value %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<a href="/nodes/{{ node.public_key }}" class="link link-hover">
|
||||
{% if ns.friendly_name or node.name %}
|
||||
<div class="font-medium">{{ ns.friendly_name or node.name }}</div>
|
||||
{% if ns.tag_name or node.name %}
|
||||
<div class="font-medium">{{ ns.tag_name or node.name }}</div>
|
||||
<div class="text-xs font-mono opacity-70">{{ node.public_key[:16] }}...</div>
|
||||
{% else %}
|
||||
<span class="font-mono text-sm">{{ node.public_key[:16] }}...</span>
|
||||
|
||||
@@ -390,3 +390,64 @@ class TestImportTags:
|
||||
assert tag_dict["is_disabled"].value_type == "boolean"
|
||||
|
||||
Path(f.name).unlink()
|
||||
|
||||
def test_import_with_clear_existing(self, db_manager):
|
||||
"""Test that clear_existing deletes all tags before importing."""
|
||||
# Create initial tags
|
||||
initial_data = {
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": {
|
||||
"old_tag": "old_value",
|
||||
"shared_tag": "old_value",
|
||||
},
|
||||
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef": {
|
||||
"another_old_tag": "value",
|
||||
},
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
yaml.dump(initial_data, f)
|
||||
f.flush()
|
||||
initial_file = f.name
|
||||
|
||||
stats1 = import_tags(initial_file, db_manager, create_nodes=True)
|
||||
assert stats1["created"] == 3
|
||||
assert stats1["deleted"] == 0
|
||||
|
||||
# Verify initial tags exist
|
||||
with db_manager.session_scope() as session:
|
||||
tags = session.execute(select(NodeTag)).scalars().all()
|
||||
assert len(tags) == 3
|
||||
|
||||
# Import new tags with clear_existing=True
|
||||
new_data = {
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": {
|
||||
"new_tag": "new_value",
|
||||
"shared_tag": "new_value",
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||
yaml.dump(new_data, f)
|
||||
f.flush()
|
||||
new_file = f.name
|
||||
|
||||
stats2 = import_tags(
|
||||
new_file, db_manager, create_nodes=True, clear_existing=True
|
||||
)
|
||||
assert stats2["deleted"] == 3 # All 3 old tags deleted
|
||||
assert stats2["created"] == 2 # 2 new tags created
|
||||
assert stats2["updated"] == 0 # No updates when clearing
|
||||
|
||||
# Verify only new tags exist
|
||||
with db_manager.session_scope() as session:
|
||||
tags = session.execute(select(NodeTag)).scalars().all()
|
||||
tag_dict = {t.key: t for t in tags}
|
||||
assert len(tags) == 2
|
||||
assert "new_tag" in tag_dict
|
||||
assert "shared_tag" in tag_dict
|
||||
assert tag_dict["shared_tag"].value == "new_value"
|
||||
assert "old_tag" not in tag_dict
|
||||
assert "another_old_tag" not in tag_dict
|
||||
|
||||
Path(initial_file).unlink()
|
||||
Path(new_file).unlink()
|
||||
|
||||
Reference in New Issue
Block a user