from datetime import datetime from sqlalchemy import ( Column, String, Text, Boolean, Integer, BigInteger, DateTime, ForeignKey, CheckConstraint, Index, JSON, ARRAY, LargeBinary, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship, declarative_base import uuid Base = declarative_base() class Project(Base): __tablename__ = "projects" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(255), unique=True, nullable=False) description = Column(Text) is_public = Column(Boolean, default=True) is_system = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) created_by = Column(String(255), nullable=False) team_id = Column(UUID(as_uuid=True), ForeignKey("teams.id", ondelete="SET NULL")) packages = relationship( "Package", back_populates="project", cascade="all, delete-orphan" ) permissions = relationship( "AccessPermission", back_populates="project", cascade="all, delete-orphan" ) team = relationship("Team", back_populates="projects") __table_args__ = ( Index("idx_projects_name", "name"), Index("idx_projects_created_by", "created_by"), Index("idx_projects_team_id", "team_id"), Index("idx_projects_is_system", "is_system"), ) class Package(Base): __tablename__ = "packages" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id = Column( UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, ) name = Column(String(255), nullable=False) description = Column(Text) format = Column(String(50), default="generic", nullable=False) platform = Column(String(50), default="any", nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) project = relationship("Project", back_populates="packages") tags = relationship("Tag", back_populates="package", cascade="all, delete-orphan") uploads = relationship( "Upload", back_populates="package", cascade="all, delete-orphan" ) consumers = relationship( "Consumer", back_populates="package", cascade="all, delete-orphan" ) versions = relationship( "PackageVersion", back_populates="package", cascade="all, delete-orphan" ) __table_args__ = ( Index("idx_packages_project_id", "project_id"), Index("idx_packages_name", "name"), Index("idx_packages_format", "format"), Index("idx_packages_platform", "platform"), Index( "idx_packages_project_name", "project_id", "name", unique=True ), # Composite unique index CheckConstraint( "format IN ('generic', 'npm', 'pypi', 'docker', 'deb', 'rpm', 'maven', 'nuget', 'helm')", name="check_package_format", ), CheckConstraint( "platform IN ('any', 'linux', 'darwin', 'windows', 'linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64')", name="check_package_platform", ), {"extend_existing": True}, ) class Artifact(Base): __tablename__ = "artifacts" id = Column(String(64), primary_key=True) # SHA256 hash size = Column(BigInteger, nullable=False) content_type = Column(String(255)) original_name = Column(String(1024)) checksum_md5 = Column(String(32)) # MD5 hash for additional verification checksum_sha1 = Column(String(40)) # SHA1 hash for compatibility s3_etag = Column(String(64)) # S3 ETag for verification artifact_metadata = Column( "metadata", JSON, default=dict ) # Format-specific metadata (column name is 'metadata') created_at = Column(DateTime(timezone=True), default=datetime.utcnow) created_by = Column(String(255), nullable=False) ref_count = Column(Integer, default=1) s3_key = Column(String(1024), nullable=False) tags = relationship("Tag", back_populates="artifact") uploads = relationship("Upload", back_populates="artifact") versions = relationship("PackageVersion", back_populates="artifact") dependencies = relationship( "ArtifactDependency", back_populates="artifact", cascade="all, delete-orphan" ) @property def sha256(self) -> str: """Alias for id - the SHA256 hash of the artifact content""" return self.id @property def format_metadata(self): """Alias for artifact_metadata - backward compatibility""" return self.artifact_metadata @format_metadata.setter def format_metadata(self, value): """Alias setter for artifact_metadata - backward compatibility""" self.artifact_metadata = value __table_args__ = ( Index("idx_artifacts_created_at", "created_at"), Index("idx_artifacts_created_by", "created_by"), Index("idx_artifacts_ref_count", "ref_count"), # For cleanup queries CheckConstraint("ref_count >= 0", name="check_ref_count_non_negative"), CheckConstraint("size > 0", name="check_size_positive"), ) class Tag(Base): __tablename__ = "tags" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) package_id = Column( UUID(as_uuid=True), ForeignKey("packages.id", ondelete="CASCADE"), nullable=False, ) name = Column(String(255), nullable=False) artifact_id = Column(String(64), ForeignKey("artifacts.id"), nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) created_by = Column(String(255), nullable=False) package = relationship("Package", back_populates="tags") artifact = relationship("Artifact", back_populates="tags") history = relationship( "TagHistory", back_populates="tag", cascade="all, delete-orphan" ) __table_args__ = ( Index("idx_tags_package_id", "package_id"), Index("idx_tags_artifact_id", "artifact_id"), Index( "idx_tags_package_name", "package_id", "name", unique=True ), # Composite unique index Index( "idx_tags_package_created_at", "package_id", "created_at" ), # For recent tags queries ) class TagHistory(Base): __tablename__ = "tag_history" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tag_id = Column( UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), nullable=False ) old_artifact_id = Column(String(64), ForeignKey("artifacts.id")) new_artifact_id = Column(String(64), ForeignKey("artifacts.id"), nullable=False) change_type = Column(String(20), nullable=False, default="update") changed_at = Column(DateTime(timezone=True), default=datetime.utcnow) changed_by = Column(String(255), nullable=False) tag = relationship("Tag", back_populates="history") __table_args__ = ( Index("idx_tag_history_tag_id", "tag_id"), Index("idx_tag_history_changed_at", "changed_at"), CheckConstraint( "change_type IN ('create', 'update', 'delete')", name="check_change_type" ), ) class PackageVersion(Base): """Immutable version record for a package-artifact relationship. Separates versions (immutable, set at upload) from tags (mutable labels). Each artifact in a package can have at most one version. """ __tablename__ = "package_versions" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) package_id = Column( UUID(as_uuid=True), ForeignKey("packages.id", ondelete="CASCADE"), nullable=False, ) artifact_id = Column(String(64), ForeignKey("artifacts.id"), nullable=False) version = Column(String(255), nullable=False) version_source = Column(String(50)) # 'explicit', 'filename', 'metadata', 'migrated_from_tag' created_at = Column(DateTime(timezone=True), default=datetime.utcnow) created_by = Column(String(255), nullable=False) package = relationship("Package", back_populates="versions") artifact = relationship("Artifact", back_populates="versions") __table_args__ = ( Index("idx_package_versions_package_id", "package_id"), Index("idx_package_versions_artifact_id", "artifact_id"), Index("idx_package_versions_package_version", "package_id", "version", unique=True), Index("idx_package_versions_package_artifact", "package_id", "artifact_id", unique=True), ) class Upload(Base): __tablename__ = "uploads" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) artifact_id = Column(String(64), ForeignKey("artifacts.id"), nullable=False) package_id = Column(UUID(as_uuid=True), ForeignKey("packages.id"), nullable=False) original_name = Column(String(1024)) tag_name = Column(String(255)) # Tag assigned during upload user_agent = Column(String(512)) # Client identification duration_ms = Column(Integer) # Upload timing in milliseconds deduplicated = Column(Boolean, default=False) # Whether artifact was deduplicated checksum_verified = Column(Boolean, default=True) # Whether checksum was verified status = Column( String(20), default="completed", nullable=False ) # pending, completed, failed error_message = Column(Text) # Error details for failed uploads client_checksum = Column(String(64)) # Client-provided SHA256 for verification uploaded_at = Column(DateTime(timezone=True), default=datetime.utcnow) uploaded_by = Column(String(255), nullable=False) source_ip = Column(String(45)) artifact = relationship("Artifact", back_populates="uploads") package = relationship("Package", back_populates="uploads") __table_args__ = ( Index("idx_uploads_artifact_id", "artifact_id"), Index("idx_uploads_package_id", "package_id"), Index("idx_uploads_uploaded_at", "uploaded_at"), Index("idx_uploads_package_uploaded_at", "package_id", "uploaded_at"), Index("idx_uploads_uploaded_by_at", "uploaded_by", "uploaded_at"), Index("idx_uploads_status", "status"), Index("idx_uploads_status_uploaded_at", "status", "uploaded_at"), CheckConstraint( "status IN ('pending', 'completed', 'failed')", name="check_upload_status" ), ) class UploadLock(Base): """Track in-progress uploads for conflict detection (409 responses).""" __tablename__ = "upload_locks" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) sha256_hash = Column(String(64), nullable=False) package_id = Column( UUID(as_uuid=True), ForeignKey("packages.id", ondelete="CASCADE"), nullable=False, ) locked_at = Column(DateTime(timezone=True), default=datetime.utcnow) locked_by = Column(String(255), nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False) __table_args__ = ( Index("idx_upload_locks_expires_at", "expires_at"), Index( "idx_upload_locks_hash_package", "sha256_hash", "package_id", unique=True ), ) class Consumer(Base): __tablename__ = "consumers" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) package_id = Column( UUID(as_uuid=True), ForeignKey("packages.id", ondelete="CASCADE"), nullable=False, ) project_url = Column(String(2048), nullable=False) last_access = Column(DateTime(timezone=True), default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) package = relationship("Package", back_populates="consumers") __table_args__ = ( Index("idx_consumers_package_id", "package_id"), Index("idx_consumers_last_access", "last_access"), ) class AccessPermission(Base): __tablename__ = "access_permissions" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id = Column( UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, ) user_id = Column(String(255), nullable=False) level = Column(String(20), nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) expires_at = Column(DateTime(timezone=True)) project = relationship("Project", back_populates="permissions") __table_args__ = ( CheckConstraint("level IN ('read', 'write', 'admin')", name="check_level"), Index("idx_access_permissions_project_id", "project_id"), Index("idx_access_permissions_user_id", "user_id"), ) class User(Base): """User account for authentication.""" __tablename__ = "users" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) username = Column(String(255), unique=True, nullable=False) password_hash = Column(String(255)) # NULL if OIDC-only user email = Column(String(255)) is_admin = Column(Boolean, default=False) is_active = Column(Boolean, default=True) must_change_password = Column(Boolean, default=False) oidc_subject = Column(String(255)) # OIDC subject claim oidc_issuer = Column(String(512)) # OIDC issuer URL created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) last_login = Column(DateTime(timezone=True)) # Relationships api_keys = relationship( "APIKey", back_populates="owner", cascade="all, delete-orphan" ) sessions = relationship( "Session", back_populates="user", cascade="all, delete-orphan" ) team_memberships = relationship( "TeamMembership", back_populates="user", cascade="all, delete-orphan" ) __table_args__ = ( Index("idx_users_username", "username"), Index("idx_users_email", "email"), Index("idx_users_oidc_subject", "oidc_subject"), ) class Session(Base): """User session for web login.""" __tablename__ = "sessions" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) token_hash = Column(String(64), unique=True, nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) expires_at = Column(DateTime(timezone=True), nullable=False) last_accessed = Column(DateTime(timezone=True), default=datetime.utcnow) user_agent = Column(String(512)) ip_address = Column(String(45)) user = relationship("User", back_populates="sessions") __table_args__ = ( Index("idx_sessions_user_id", "user_id"), Index("idx_sessions_token_hash", "token_hash"), Index("idx_sessions_expires_at", "expires_at"), ) class AuthSettings(Base): """Authentication settings for OIDC configuration.""" __tablename__ = "auth_settings" key = Column(String(255), primary_key=True) value = Column(Text, nullable=False) updated_at = Column(DateTime(timezone=True), default=datetime.utcnow) class APIKey(Base): __tablename__ = "api_keys" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) key_hash = Column(String(64), unique=True, nullable=False) name = Column(String(255), nullable=False) user_id = Column( String(255), nullable=False ) # Legacy field, kept for compatibility owner_id = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=True, # Nullable for migration compatibility ) description = Column(Text) scopes = Column(ARRAY(String), default=["read", "write"]) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) expires_at = Column(DateTime(timezone=True)) last_used = Column(DateTime(timezone=True)) owner = relationship("User", back_populates="api_keys") __table_args__ = ( Index("idx_api_keys_user_id", "user_id"), Index("idx_api_keys_key_hash", "key_hash"), Index("idx_api_keys_owner_id", "owner_id"), ) class AuditLog(Base): __tablename__ = "audit_logs" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) action = Column(String(100), nullable=False) resource = Column(String(1024), nullable=False) user_id = Column(String(255), nullable=False) details = Column(JSON) timestamp = Column(DateTime(timezone=True), default=datetime.utcnow) source_ip = Column(String(45)) __table_args__ = ( Index("idx_audit_logs_action", "action"), Index("idx_audit_logs_resource", "resource"), Index("idx_audit_logs_user_id", "user_id"), Index("idx_audit_logs_timestamp", "timestamp"), Index("idx_audit_logs_resource_timestamp", "resource", "timestamp"), Index("idx_audit_logs_user_timestamp", "user_id", "timestamp"), ) class ProjectHistory(Base): """Track changes to project metadata over time.""" __tablename__ = "project_history" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id = Column( UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, ) field_name = Column(String(100), nullable=False) old_value = Column(Text) new_value = Column(Text) changed_at = Column(DateTime(timezone=True), default=datetime.utcnow) changed_by = Column(String(255), nullable=False) __table_args__ = ( Index("idx_project_history_project_id", "project_id"), Index("idx_project_history_changed_at", "changed_at"), Index("idx_project_history_project_changed_at", "project_id", "changed_at"), ) class PackageHistory(Base): """Track changes to package metadata over time.""" __tablename__ = "package_history" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) package_id = Column( UUID(as_uuid=True), ForeignKey("packages.id", ondelete="CASCADE"), nullable=False, ) field_name = Column(String(100), nullable=False) old_value = Column(Text) new_value = Column(Text) changed_at = Column(DateTime(timezone=True), default=datetime.utcnow) changed_by = Column(String(255), nullable=False) __table_args__ = ( Index("idx_package_history_package_id", "package_id"), Index("idx_package_history_changed_at", "changed_at"), Index("idx_package_history_package_changed_at", "package_id", "changed_at"), ) class ArtifactDependency(Base): """Dependency declared by an artifact on another package. Each artifact can declare dependencies on other packages, specifying either an exact version or a tag. This enables recursive dependency resolution. """ __tablename__ = "artifact_dependencies" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) artifact_id = Column( String(64), ForeignKey("artifacts.id", ondelete="CASCADE"), nullable=False, ) dependency_project = Column(String(255), nullable=False) dependency_package = Column(String(255), nullable=False) version_constraint = Column(String(255), nullable=True) tag_constraint = Column(String(255), nullable=True) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) # Relationship to the artifact that declares this dependency artifact = relationship("Artifact", back_populates="dependencies") __table_args__ = ( # Exactly one of version_constraint or tag_constraint must be set CheckConstraint( "(version_constraint IS NOT NULL AND tag_constraint IS NULL) OR " "(version_constraint IS NULL AND tag_constraint IS NOT NULL)", name="check_constraint_type", ), # Each artifact can only depend on a specific project/package once Index( "idx_artifact_dependencies_artifact_id", "artifact_id", ), Index( "idx_artifact_dependencies_target", "dependency_project", "dependency_package", ), Index( "idx_artifact_dependencies_unique", "artifact_id", "dependency_project", "dependency_package", unique=True, ), ) class Team(Base): """Team for organizing projects and users.""" __tablename__ = "teams" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(255), nullable=False) slug = Column(String(255), unique=True, nullable=False) description = Column(Text) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) created_by = Column(String(255), nullable=False) settings = Column(JSON, default=dict) # Relationships memberships = relationship( "TeamMembership", back_populates="team", cascade="all, delete-orphan" ) projects = relationship("Project", back_populates="team") __table_args__ = ( Index("idx_teams_slug", "slug"), Index("idx_teams_created_by", "created_by"), Index("idx_teams_created_at", "created_at"), CheckConstraint( "slug ~ '^[a-z0-9][a-z0-9-]*[a-z0-9]$' OR slug ~ '^[a-z0-9]$'", name="check_team_slug_format", ), ) class TeamMembership(Base): """Maps users to teams with their roles.""" __tablename__ = "team_memberships" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) team_id = Column( UUID(as_uuid=True), ForeignKey("teams.id", ondelete="CASCADE"), nullable=False, ) user_id = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) role = Column(String(20), nullable=False, default="member") created_at = Column(DateTime(timezone=True), default=datetime.utcnow) invited_by = Column(String(255)) # Relationships team = relationship("Team", back_populates="memberships") user = relationship("User", back_populates="team_memberships") __table_args__ = ( Index("idx_team_memberships_team_id", "team_id"), Index("idx_team_memberships_user_id", "user_id"), Index("idx_team_memberships_role", "role"), Index("idx_team_memberships_team_role", "team_id", "role"), Index("idx_team_memberships_unique", "team_id", "user_id", unique=True), CheckConstraint( "role IN ('owner', 'admin', 'member')", name="check_team_role", ), ) # ============================================================================= # Upstream Caching Models # ============================================================================= # Valid source types for upstream registries SOURCE_TYPES = ["npm", "pypi", "maven", "docker", "helm", "nuget", "deb", "rpm", "generic"] # Valid authentication types AUTH_TYPES = ["none", "basic", "bearer", "api_key"] class UpstreamSource(Base): """Configuration for an upstream artifact registry. Stores connection details and authentication for upstream registries like npm, PyPI, Maven Central, or private Artifactory instances. """ __tablename__ = "upstream_sources" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(255), unique=True, nullable=False) source_type = Column(String(50), default="generic", nullable=False) url = Column(String(2048), nullable=False) enabled = Column(Boolean, default=False, nullable=False) auth_type = Column(String(20), default="none", nullable=False) username = Column(String(255)) password_encrypted = Column(LargeBinary) headers_encrypted = Column(LargeBinary) priority = Column(Integer, default=100, nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) # Relationships cached_urls = relationship("CachedUrl", back_populates="source") __table_args__ = ( Index("idx_upstream_sources_enabled", "enabled"), Index("idx_upstream_sources_source_type", "source_type"), Index("idx_upstream_sources_priority", "priority"), CheckConstraint( "source_type IN ('npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic')", name="check_source_type", ), CheckConstraint( "auth_type IN ('none', 'basic', 'bearer', 'api_key')", name="check_auth_type", ), CheckConstraint("priority > 0", name="check_priority_positive"), ) def set_password(self, password: str) -> None: """Encrypt and store a password/token.""" from .encryption import encrypt_value if password: self.password_encrypted = encrypt_value(password) else: self.password_encrypted = None def get_password(self) -> str | None: """Decrypt and return the stored password/token.""" from .encryption import decrypt_value if self.password_encrypted: try: return decrypt_value(self.password_encrypted) except Exception: return None return None def has_password(self) -> bool: """Check if a password/token is stored.""" return self.password_encrypted is not None def set_headers(self, headers: dict) -> None: """Encrypt and store custom headers as JSON.""" from .encryption import encrypt_value import json if headers: self.headers_encrypted = encrypt_value(json.dumps(headers)) else: self.headers_encrypted = None def get_headers(self) -> dict | None: """Decrypt and return custom headers.""" from .encryption import decrypt_value import json if self.headers_encrypted: try: return json.loads(decrypt_value(self.headers_encrypted)) except Exception: return None return None class CacheSettings(Base): """Global cache settings (singleton table). Controls behavior of the upstream caching system. """ __tablename__ = "cache_settings" id = Column(Integer, primary_key=True, default=1) auto_create_system_projects = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column( DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow ) __table_args__ = ( CheckConstraint("id = 1", name="check_cache_settings_singleton"), ) class CachedUrl(Base): """Tracks URL to artifact mappings for provenance. Records which URLs have been cached and maps them to their stored artifacts. Enables "is this URL already cached?" lookups and audit trails. """ __tablename__ = "cached_urls" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) url = Column(String(4096), nullable=False) url_hash = Column(String(64), unique=True, nullable=False) artifact_id = Column( String(64), ForeignKey("artifacts.id"), nullable=False ) source_id = Column( UUID(as_uuid=True), ForeignKey("upstream_sources.id", ondelete="SET NULL"), ) fetched_at = Column(DateTime(timezone=True), default=datetime.utcnow, nullable=False) response_headers = Column(JSON, default=dict) created_at = Column(DateTime(timezone=True), default=datetime.utcnow) # Relationships artifact = relationship("Artifact") source = relationship("UpstreamSource", back_populates="cached_urls") __table_args__ = ( Index("idx_cached_urls_url_hash", "url_hash"), Index("idx_cached_urls_artifact_id", "artifact_id"), Index("idx_cached_urls_source_id", "source_id"), Index("idx_cached_urls_fetched_at", "fetched_at"), ) @staticmethod def compute_url_hash(url: str) -> str: """Compute SHA256 hash of a URL for fast lookups.""" import hashlib return hashlib.sha256(url.encode("utf-8")).hexdigest()