mbuzz 0.1.0__py3-none-any.whl

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.
mbuzz/__init__.py ADDED
@@ -0,0 +1,87 @@
1
+ """Mbuzz - Multi-touch attribution SDK for Python."""
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from .config import config
6
+ from .context import get_context
7
+ from .client.track import track, TrackResult
8
+ from .client.identify import identify
9
+ from .client.conversion import conversion, ConversionResult
10
+
11
+ __version__ = "0.1.0"
12
+
13
+
14
+ def init(
15
+ api_key: str,
16
+ api_url: Optional[str] = None,
17
+ enabled: bool = True,
18
+ debug: bool = False,
19
+ timeout: Optional[float] = None,
20
+ skip_paths: Optional[list] = None,
21
+ skip_extensions: Optional[list] = None,
22
+ ) -> None:
23
+ """Initialize the mbuzz SDK.
24
+
25
+ Args:
26
+ api_key: Your mbuzz API key (required)
27
+ api_url: API endpoint URL (default: https://mbuzz.co/api/v1)
28
+ enabled: Enable/disable tracking (default: True)
29
+ debug: Enable debug logging (default: False)
30
+ timeout: Request timeout in seconds (default: 5.0)
31
+ skip_paths: Additional paths to skip tracking
32
+ skip_extensions: Additional file extensions to skip
33
+ """
34
+ config.init(
35
+ api_key=api_key,
36
+ api_url=api_url,
37
+ enabled=enabled,
38
+ debug=debug,
39
+ timeout=timeout,
40
+ skip_paths=skip_paths or [],
41
+ skip_extensions=skip_extensions or [],
42
+ )
43
+
44
+
45
+ def event(event_type: str, **properties: Any) -> TrackResult:
46
+ """Track an event.
47
+
48
+ Args:
49
+ event_type: Type of event (e.g., "page_view", "button_click")
50
+ **properties: Additional event properties
51
+
52
+ Returns:
53
+ TrackResult with success status and event details
54
+ """
55
+ return track(event_type=event_type, properties=properties)
56
+
57
+
58
+ def visitor_id() -> Optional[str]:
59
+ """Get current visitor ID from context."""
60
+ ctx = get_context()
61
+ return ctx.visitor_id if ctx else None
62
+
63
+
64
+ def session_id() -> Optional[str]:
65
+ """Get current session ID from context."""
66
+ ctx = get_context()
67
+ return ctx.session_id if ctx else None
68
+
69
+
70
+ def user_id() -> Optional[str]:
71
+ """Get current user ID from context."""
72
+ ctx = get_context()
73
+ return ctx.user_id if ctx else None
74
+
75
+
76
+ __all__ = [
77
+ "init",
78
+ "event",
79
+ "conversion",
80
+ "identify",
81
+ "visitor_id",
82
+ "session_id",
83
+ "user_id",
84
+ "TrackResult",
85
+ "ConversionResult",
86
+ "__version__",
87
+ ]
mbuzz/api.py ADDED
@@ -0,0 +1,71 @@
1
+ """HTTP client for mbuzz API communication."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any, Dict, Optional
6
+ from urllib.request import Request, urlopen
7
+ from urllib.error import URLError, HTTPError
8
+
9
+ from .config import config
10
+
11
+ logger = logging.getLogger("mbuzz")
12
+
13
+ VERSION = "0.1.0"
14
+
15
+
16
+ def post(path: str, payload: Dict[str, Any]) -> bool:
17
+ """POST to API, return True on success, False on any failure.
18
+
19
+ Never raises exceptions - all errors are caught and logged.
20
+ """
21
+ if not config._initialized or not config.enabled:
22
+ return False
23
+
24
+ try:
25
+ response = _make_request(path, payload)
26
+ return 200 <= response.status < 300
27
+ except Exception as e:
28
+ if config.debug:
29
+ logger.error(f"mbuzz API error: {e}")
30
+ return False
31
+
32
+
33
+ def post_with_response(path: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
34
+ """POST to API, return parsed JSON on success, None on failure.
35
+
36
+ Never raises exceptions - all errors are caught and logged.
37
+ """
38
+ if not config._initialized or not config.enabled:
39
+ return None
40
+
41
+ try:
42
+ response = _make_request(path, payload)
43
+ if 200 <= response.status < 300:
44
+ body = response.read().decode("utf-8")
45
+ if config.debug:
46
+ logger.debug(f"mbuzz response: {body}")
47
+ return json.loads(body)
48
+ return None
49
+ except Exception as e:
50
+ if config.debug:
51
+ logger.error(f"mbuzz API error: {e}")
52
+ return None
53
+
54
+
55
+ def _make_request(path: str, payload: Dict[str, Any]):
56
+ """Make HTTP request to API."""
57
+ base_url = config.api_url.rstrip("/")
58
+ clean_path = path.lstrip("/")
59
+ url = f"{base_url}/{clean_path}"
60
+
61
+ data = json.dumps(payload).encode("utf-8")
62
+
63
+ req = Request(url, data=data, method="POST")
64
+ req.add_header("Authorization", f"Bearer {config.api_key}")
65
+ req.add_header("Content-Type", "application/json")
66
+ req.add_header("User-Agent", f"mbuzz-python/{VERSION}")
67
+
68
+ if config.debug:
69
+ logger.debug(f"mbuzz POST {url}: {payload}")
70
+
71
+ return urlopen(req, timeout=config.timeout)
@@ -0,0 +1 @@
1
+ """Client module - stub for TDD."""
@@ -0,0 +1,74 @@
1
+ """Conversion request for tracking conversions."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from ..api import post_with_response
7
+ from ..context import get_context
8
+
9
+
10
+ @dataclass
11
+ class ConversionResult:
12
+ """Result of tracking a conversion."""
13
+
14
+ success: bool
15
+ conversion_id: Optional[str] = None
16
+ attribution: Optional[Dict[str, Any]] = None
17
+
18
+
19
+ def conversion(
20
+ conversion_type: str,
21
+ visitor_id: Optional[str] = None,
22
+ user_id: Optional[Union[str, int]] = None,
23
+ event_id: Optional[str] = None,
24
+ revenue: Optional[float] = None,
25
+ currency: str = "USD",
26
+ is_acquisition: bool = False,
27
+ inherit_acquisition: bool = False,
28
+ properties: Optional[Dict[str, Any]] = None,
29
+ ) -> ConversionResult:
30
+ """Track a conversion.
31
+
32
+ Args:
33
+ conversion_type: Type of conversion (e.g., "purchase", "signup")
34
+ visitor_id: Visitor ID (uses context if not provided)
35
+ user_id: User ID (uses context if not provided)
36
+ event_id: Optional event ID to link conversion to
37
+ revenue: Revenue amount
38
+ currency: Currency code (default: USD)
39
+ is_acquisition: Whether this is a customer acquisition
40
+ inherit_acquisition: Whether to inherit acquisition from previous conversion
41
+ properties: Additional conversion properties
42
+
43
+ Returns:
44
+ ConversionResult with success status, conversion ID, and attribution data
45
+ """
46
+ ctx = get_context()
47
+
48
+ visitor_id = visitor_id or (ctx.visitor_id if ctx else None)
49
+ user_id = user_id or (ctx.user_id if ctx else None)
50
+
51
+ if not visitor_id and not user_id:
52
+ return ConversionResult(success=False)
53
+
54
+ payload = {
55
+ "conversion_type": conversion_type,
56
+ "visitor_id": visitor_id,
57
+ "user_id": str(user_id) if user_id else None,
58
+ "event_id": event_id,
59
+ "revenue": revenue,
60
+ "currency": currency,
61
+ "is_acquisition": is_acquisition,
62
+ "inherit_acquisition": inherit_acquisition,
63
+ "properties": properties or {},
64
+ }
65
+
66
+ response = post_with_response("/conversions", payload)
67
+ if not response:
68
+ return ConversionResult(success=False)
69
+
70
+ return ConversionResult(
71
+ success=True,
72
+ conversion_id=response.get("conversion", {}).get("id"),
73
+ attribution=response.get("attribution"),
74
+ )
@@ -0,0 +1,36 @@
1
+ """Identify request for user identification."""
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from ..api import post
6
+ from ..context import get_context
7
+
8
+
9
+ def identify(
10
+ user_id: Union[str, int, None],
11
+ visitor_id: Optional[str] = None,
12
+ traits: Optional[Dict[str, Any]] = None,
13
+ ) -> bool:
14
+ """Identify a user and link to visitor.
15
+
16
+ Args:
17
+ user_id: User ID to identify (required)
18
+ visitor_id: Visitor ID (uses context if not provided)
19
+ traits: User traits/attributes
20
+
21
+ Returns:
22
+ True on success, False on failure
23
+ """
24
+ if not user_id:
25
+ return False
26
+
27
+ ctx = get_context()
28
+ visitor_id = visitor_id or (ctx.visitor_id if ctx else None)
29
+
30
+ payload = {
31
+ "user_id": str(user_id),
32
+ "visitor_id": visitor_id,
33
+ "traits": traits or {},
34
+ }
35
+
36
+ return post("/identify", payload)
@@ -0,0 +1,38 @@
1
+ """Session request for creating sessions."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Optional
5
+
6
+ from ..api import post
7
+
8
+
9
+ def create_session(
10
+ visitor_id: str,
11
+ session_id: str,
12
+ url: str,
13
+ referrer: Optional[str] = None,
14
+ ) -> bool:
15
+ """Create a new session.
16
+
17
+ Called async from middleware on first request.
18
+
19
+ Args:
20
+ visitor_id: Visitor ID
21
+ session_id: Session ID
22
+ url: Current page URL
23
+ referrer: Referring URL
24
+
25
+ Returns:
26
+ True on success, False on failure
27
+ """
28
+ payload = {
29
+ "session": {
30
+ "visitor_id": visitor_id,
31
+ "session_id": session_id,
32
+ "url": url,
33
+ "referrer": referrer,
34
+ "started_at": datetime.now(timezone.utc).isoformat(),
35
+ }
36
+ }
37
+
38
+ return post("/sessions", payload)
mbuzz/client/track.py ADDED
@@ -0,0 +1,130 @@
1
+ """Track request for event tracking."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, Optional
6
+
7
+ from ..api import post_with_response
8
+ from ..context import get_context
9
+
10
+
11
+ @dataclass
12
+ class TrackResult:
13
+ """Result of tracking an event."""
14
+
15
+ success: bool
16
+ event_id: Optional[str] = None
17
+ event_type: Optional[str] = None
18
+ visitor_id: Optional[str] = None
19
+ session_id: Optional[str] = None
20
+
21
+
22
+ @dataclass
23
+ class TrackOptions:
24
+ """Options for tracking an event."""
25
+
26
+ event_type: str
27
+ visitor_id: Optional[str] = None
28
+ session_id: Optional[str] = None
29
+ user_id: Optional[str] = None
30
+ properties: Optional[Dict[str, Any]] = None
31
+
32
+
33
+ def _resolve_ids(options: TrackOptions) -> TrackOptions:
34
+ """Resolve visitor/session/user IDs from context if not provided."""
35
+ ctx = get_context()
36
+ if not ctx:
37
+ return options
38
+
39
+ return TrackOptions(
40
+ event_type=options.event_type,
41
+ visitor_id=options.visitor_id or ctx.visitor_id,
42
+ session_id=options.session_id or ctx.session_id,
43
+ user_id=options.user_id or ctx.user_id,
44
+ properties=options.properties,
45
+ )
46
+
47
+
48
+ def _enrich_properties(options: TrackOptions) -> Dict[str, Any]:
49
+ """Enrich properties with url/referrer from context."""
50
+ ctx = get_context()
51
+ props = options.properties or {}
52
+
53
+ if ctx:
54
+ return ctx.enrich_properties(props)
55
+ return props
56
+
57
+
58
+ def _validate(options: TrackOptions) -> bool:
59
+ """Validate track options. Must have visitor_id or user_id."""
60
+ return bool(options.visitor_id or options.user_id)
61
+
62
+
63
+ def _build_payload(options: TrackOptions, properties: Dict[str, Any]) -> Dict[str, Any]:
64
+ """Build API payload from options."""
65
+ return {
66
+ "events": [
67
+ {
68
+ "event_type": options.event_type,
69
+ "visitor_id": options.visitor_id,
70
+ "session_id": options.session_id,
71
+ "user_id": options.user_id,
72
+ "properties": properties,
73
+ "timestamp": datetime.now(timezone.utc).isoformat(),
74
+ }
75
+ ]
76
+ }
77
+
78
+
79
+ def _parse_response(response: Optional[Dict[str, Any]], options: TrackOptions) -> TrackResult:
80
+ """Parse API response into TrackResult."""
81
+ if not response or not response.get("events"):
82
+ return TrackResult(success=False)
83
+
84
+ event = response["events"][0]
85
+ return TrackResult(
86
+ success=True,
87
+ event_id=event.get("id"),
88
+ event_type=options.event_type,
89
+ visitor_id=options.visitor_id,
90
+ session_id=options.session_id,
91
+ )
92
+
93
+
94
+ def track(
95
+ event_type: str,
96
+ visitor_id: Optional[str] = None,
97
+ session_id: Optional[str] = None,
98
+ user_id: Optional[str] = None,
99
+ properties: Optional[Dict[str, Any]] = None,
100
+ ) -> TrackResult:
101
+ """Track an event.
102
+
103
+ Args:
104
+ event_type: Type of event (e.g., "page_view", "button_click")
105
+ visitor_id: Visitor ID (uses context if not provided)
106
+ session_id: Session ID (uses context if not provided)
107
+ user_id: User ID (uses context if not provided)
108
+ properties: Additional event properties
109
+
110
+ Returns:
111
+ TrackResult with success status and event details
112
+ """
113
+ options = TrackOptions(
114
+ event_type=event_type,
115
+ visitor_id=visitor_id,
116
+ session_id=session_id,
117
+ user_id=user_id,
118
+ properties=properties,
119
+ )
120
+
121
+ options = _resolve_ids(options)
122
+
123
+ if not _validate(options):
124
+ return TrackResult(success=False)
125
+
126
+ enriched_props = _enrich_properties(options)
127
+ payload = _build_payload(options, enriched_props)
128
+ response = post_with_response("/events", payload)
129
+
130
+ return _parse_response(response, options)
mbuzz/config.py ADDED
@@ -0,0 +1,96 @@
1
+ """Configuration management for mbuzz SDK."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Optional
5
+
6
+ DEFAULT_API_URL = "https://mbuzz.co/api/v1"
7
+ DEFAULT_TIMEOUT = 5.0
8
+
9
+ DEFAULT_SKIP_PATHS = [
10
+ "/health",
11
+ "/healthz",
12
+ "/ping",
13
+ "/up",
14
+ "/static",
15
+ "/assets",
16
+ "/media",
17
+ "/admin/jsi18n",
18
+ "/__debug__",
19
+ ]
20
+
21
+ DEFAULT_SKIP_EXTENSIONS = [
22
+ ".js",
23
+ ".css",
24
+ ".map",
25
+ ".png",
26
+ ".jpg",
27
+ ".jpeg",
28
+ ".gif",
29
+ ".ico",
30
+ ".svg",
31
+ ".webp",
32
+ ".woff",
33
+ ".woff2",
34
+ ".ttf",
35
+ ".eot",
36
+ ]
37
+
38
+
39
+ @dataclass
40
+ class Config:
41
+ """SDK configuration singleton."""
42
+
43
+ api_key: str = ""
44
+ api_url: str = DEFAULT_API_URL
45
+ enabled: bool = True
46
+ debug: bool = False
47
+ timeout: float = DEFAULT_TIMEOUT
48
+ skip_paths: List[str] = field(default_factory=list)
49
+ skip_extensions: List[str] = field(default_factory=list)
50
+ _initialized: bool = False
51
+
52
+ def init(
53
+ self,
54
+ api_key: str,
55
+ api_url: Optional[str] = None,
56
+ enabled: bool = True,
57
+ debug: bool = False,
58
+ timeout: Optional[float] = None,
59
+ skip_paths: Optional[List[str]] = None,
60
+ skip_extensions: Optional[List[str]] = None,
61
+ ) -> None:
62
+ """Initialize configuration."""
63
+ if not api_key:
64
+ raise ValueError("api_key is required")
65
+
66
+ self.api_key = api_key
67
+ self.api_url = api_url or DEFAULT_API_URL
68
+ self.enabled = enabled
69
+ self.debug = debug
70
+ self.timeout = timeout or DEFAULT_TIMEOUT
71
+ self.skip_paths = DEFAULT_SKIP_PATHS + (skip_paths or [])
72
+ self.skip_extensions = DEFAULT_SKIP_EXTENSIONS + (skip_extensions or [])
73
+ self._initialized = True
74
+
75
+ def should_skip_path(self, path: str) -> bool:
76
+ """Check if path should be skipped from tracking."""
77
+ if any(path.startswith(skip) for skip in self.skip_paths):
78
+ return True
79
+ if any(path.endswith(ext) for ext in self.skip_extensions):
80
+ return True
81
+ return False
82
+
83
+ def reset(self) -> None:
84
+ """Reset configuration (for testing)."""
85
+ self.api_key = ""
86
+ self.api_url = DEFAULT_API_URL
87
+ self.enabled = True
88
+ self.debug = False
89
+ self.timeout = DEFAULT_TIMEOUT
90
+ self.skip_paths = []
91
+ self.skip_extensions = []
92
+ self._initialized = False
93
+
94
+
95
+ # Singleton instance
96
+ config = Config()
mbuzz/context.py ADDED
@@ -0,0 +1,45 @@
1
+ """Request context management using contextvars."""
2
+
3
+ from contextvars import ContextVar
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ @dataclass
9
+ class RequestContext:
10
+ """Holds request-scoped data for tracking."""
11
+
12
+ visitor_id: str
13
+ session_id: str
14
+ user_id: Optional[str] = None
15
+ url: Optional[str] = None
16
+ referrer: Optional[str] = None
17
+
18
+ def enrich_properties(self, properties: Dict[str, Any]) -> Dict[str, Any]:
19
+ """Add url and referrer to properties if not already present."""
20
+ result: Dict[str, Any] = {}
21
+ if self.url:
22
+ result["url"] = self.url
23
+ if self.referrer:
24
+ result["referrer"] = self.referrer
25
+ result.update(properties)
26
+ return result
27
+
28
+
29
+ # Context variable for current request
30
+ _context: ContextVar[Optional[RequestContext]] = ContextVar("mbuzz_context", default=None)
31
+
32
+
33
+ def get_context() -> Optional[RequestContext]:
34
+ """Get the current request context."""
35
+ return _context.get()
36
+
37
+
38
+ def set_context(ctx: RequestContext) -> None:
39
+ """Set the current request context."""
40
+ _context.set(ctx)
41
+
42
+
43
+ def clear_context() -> None:
44
+ """Clear the current request context."""
45
+ _context.set(None)
mbuzz/cookies.py ADDED
@@ -0,0 +1,7 @@
1
+ """Cookie constants for mbuzz SDK."""
2
+
3
+ VISITOR_COOKIE = "_mbuzz_vid"
4
+ SESSION_COOKIE = "_mbuzz_sid"
5
+
6
+ VISITOR_MAX_AGE = 63072000 # 2 years in seconds
7
+ SESSION_MAX_AGE = 1800 # 30 minutes in seconds
@@ -0,0 +1 @@
1
+ """Middleware for web frameworks."""
@@ -0,0 +1,119 @@
1
+ """Flask middleware for mbuzz tracking."""
2
+
3
+ import threading
4
+ from flask import Flask, request, g, Response
5
+ from typing import Optional
6
+
7
+ from ..config import config
8
+ from ..context import RequestContext, set_context, clear_context
9
+ from ..cookies import VISITOR_COOKIE, SESSION_COOKIE, VISITOR_MAX_AGE, SESSION_MAX_AGE
10
+ from ..utils.identifier import generate_id
11
+ from ..client.session import create_session
12
+
13
+
14
+ def init_app(app: Flask) -> None:
15
+ """Initialize mbuzz tracking for Flask app."""
16
+
17
+ @app.before_request
18
+ def before_request():
19
+ if _should_skip():
20
+ return
21
+
22
+ visitor_id = _get_or_create_visitor_id()
23
+ session_id = _get_or_create_session_id()
24
+ is_new_session = SESSION_COOKIE not in request.cookies
25
+
26
+ _set_request_context(visitor_id, session_id)
27
+ _store_in_g(visitor_id, session_id, is_new_session)
28
+ _create_session_if_new(visitor_id, session_id, is_new_session)
29
+
30
+ @app.after_request
31
+ def after_request(response: Response) -> Response:
32
+ if not hasattr(g, "mbuzz_visitor_id"):
33
+ return response
34
+
35
+ _set_cookies(response)
36
+ return response
37
+
38
+ @app.teardown_request
39
+ def teardown_request(exception=None):
40
+ clear_context()
41
+
42
+
43
+ def _should_skip() -> bool:
44
+ """Check if request should skip tracking."""
45
+ if not config._initialized or not config.enabled:
46
+ return True
47
+ if config.should_skip_path(request.path):
48
+ return True
49
+ return False
50
+
51
+
52
+ def _get_or_create_visitor_id() -> str:
53
+ """Get visitor ID from cookie or generate new one."""
54
+ return request.cookies.get(VISITOR_COOKIE) or generate_id()
55
+
56
+
57
+ def _get_or_create_session_id() -> str:
58
+ """Get session ID from cookie or generate new one."""
59
+ return request.cookies.get(SESSION_COOKIE) or generate_id()
60
+
61
+
62
+ def _set_request_context(visitor_id: str, session_id: str) -> None:
63
+ """Set request context for tracking calls."""
64
+ ctx = RequestContext(
65
+ visitor_id=visitor_id,
66
+ session_id=session_id,
67
+ user_id=None,
68
+ url=request.url,
69
+ referrer=request.referrer,
70
+ )
71
+ set_context(ctx)
72
+
73
+
74
+ def _store_in_g(visitor_id: str, session_id: str, is_new_session: bool) -> None:
75
+ """Store tracking IDs in Flask g object for after_request."""
76
+ g.mbuzz_visitor_id = visitor_id
77
+ g.mbuzz_session_id = session_id
78
+ g.mbuzz_is_new_visitor = VISITOR_COOKIE not in request.cookies
79
+ g.mbuzz_is_new_session = is_new_session
80
+
81
+
82
+ def _create_session_if_new(visitor_id: str, session_id: str, is_new_session: bool) -> None:
83
+ """Create session asynchronously if new session."""
84
+ if not is_new_session:
85
+ return
86
+
87
+ ctx = RequestContext(
88
+ visitor_id=visitor_id,
89
+ session_id=session_id,
90
+ url=request.url,
91
+ referrer=request.referrer,
92
+ )
93
+ threading.Thread(
94
+ target=create_session,
95
+ args=(visitor_id, session_id, ctx.url, ctx.referrer),
96
+ daemon=True
97
+ ).start()
98
+
99
+
100
+ def _set_cookies(response: Response) -> None:
101
+ """Set visitor and session cookies on response."""
102
+ secure = request.is_secure
103
+
104
+ response.set_cookie(
105
+ VISITOR_COOKIE,
106
+ g.mbuzz_visitor_id,
107
+ max_age=VISITOR_MAX_AGE,
108
+ httponly=True,
109
+ samesite="Lax",
110
+ secure=secure,
111
+ )
112
+ response.set_cookie(
113
+ SESSION_COOKIE,
114
+ g.mbuzz_session_id,
115
+ max_age=SESSION_MAX_AGE,
116
+ httponly=True,
117
+ samesite="Lax",
118
+ secure=secure,
119
+ )
@@ -0,0 +1,5 @@
1
+ """Utility functions for mbuzz SDK."""
2
+
3
+ from .identifier import generate_id
4
+
5
+ __all__ = ["generate_id"]
@@ -0,0 +1,8 @@
1
+ """ID generation utilities."""
2
+
3
+ import secrets
4
+
5
+
6
+ def generate_id() -> str:
7
+ """Generate 64-character hex string (256 bits of entropy)."""
8
+ return secrets.token_hex(32)
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: mbuzz
3
+ Version: 0.1.0
4
+ Summary: Multi-touch attribution SDK for Python
5
+ Project-URL: Homepage, https://mbuzz.co
6
+ Project-URL: Documentation, https://mbuzz.co/docs/getting-started
7
+ Project-URL: Repository, https://github.com/mbuzzco/mbuzz-python
8
+ Author-email: Mbuzz <support@mbuzz.co>
9
+ License-Expression: MIT
10
+ Keywords: analytics,attribution,marketing,tracking
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Flask
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Requires-Python: >=3.8
24
+ Provides-Extra: dev
25
+ Requires-Dist: black; extra == 'dev'
26
+ Requires-Dist: mypy; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: pytest-cov; extra == 'dev'
29
+ Requires-Dist: ruff; extra == 'dev'
30
+ Provides-Extra: django
31
+ Provides-Extra: fastapi
32
+ Requires-Dist: starlette; extra == 'fastapi'
33
+ Provides-Extra: flask
34
+ Description-Content-Type: text/markdown
35
+
36
+ # mbuzz-python
37
+
38
+ Python SDK for mbuzz multi-touch attribution.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install mbuzz
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import mbuzz
50
+
51
+ # Initialize (once on app start)
52
+ mbuzz.init(api_key="sk_live_...")
53
+
54
+ # Track events
55
+ mbuzz.event("page_view", url="/pricing")
56
+
57
+ # Track conversions
58
+ mbuzz.conversion("purchase", revenue=99.99)
59
+
60
+ # Identify users
61
+ mbuzz.identify("user_123", traits={"email": "user@example.com"})
62
+ ```
63
+
64
+ ## Documentation
65
+
66
+ See [mbuzz.co/docs/getting-started](https://mbuzz.co/docs/getting-started) for full documentation.
@@ -0,0 +1,17 @@
1
+ mbuzz/__init__.py,sha256=zZls_Zf7HfuCFUYnsG2e2mkvDNCioX0togiHom8kyUg,2247
2
+ mbuzz/api.py,sha256=BITkW6TQEGmPA0OmYFFC611X6JWiY6C1dY-_wsKDT2U,2107
3
+ mbuzz/config.py,sha256=u6v1WnMTgiqyDXHszlXw5hyNENgYRy7-z-IWKuYp0-o,2399
4
+ mbuzz/context.py,sha256=l_mwJg9rcmJ0viz9xTFvI5JP8Imohv5o6EjduAnBCNw,1224
5
+ mbuzz/cookies.py,sha256=MCpMljQXaqJ2bhEtTv1aKR2fuqLfGBFofjPRayAQGpw,197
6
+ mbuzz/client/__init__.py,sha256=oNOTpPKdt-b_VH_f848OQTbEEDFhlpbEjZ-WjKvh3JI,36
7
+ mbuzz/client/conversion.py,sha256=Ua3ngFWmH6ekm3ZanNf5up_jV8t5nf788ipM1usvZoM,2346
8
+ mbuzz/client/identify.py,sha256=duyd-OWdBo4w7-7Nv5X-GeG2IpMI-1G_kYwnDXup0FE,859
9
+ mbuzz/client/session.py,sha256=mk8v892bvpeW-VNKFQYDsW442w881yfXBiM4sr9a0SU,835
10
+ mbuzz/client/track.py,sha256=-leW3yJcg9Vm6dL7fLQvFwgUN-nZF-OmDRMeR_9Blz8,3725
11
+ mbuzz/middleware/__init__.py,sha256=gIjwTArToaQNB2NC0iPE_RcmzuHjH7-7jjd7_Dyq4Pw,37
12
+ mbuzz/middleware/flask.py,sha256=dRn8rnOTSrd0w7GjwzjxFFFGuCe7J84MvaNGTT5w4_k,3453
13
+ mbuzz/utils/__init__.py,sha256=-ejtRN6CTkV3D8uujDtAPDoMT0DAG-ULpWOF-Ae55dY,103
14
+ mbuzz/utils/identifier.py,sha256=iAYmd4he2RTtTNlmszb6KQmD9McZbSMgZZdkMuoGOCE,174
15
+ mbuzz-0.1.0.dist-info/METADATA,sha256=HvgtdBRMTA3QqdpOrlkMUqIMoeq5LKnYPUuNrnwF9Hs,1849
16
+ mbuzz-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ mbuzz-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any