mbuzz 0.1.0__tar.gz

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-0.1.0/.gitignore ADDED
@@ -0,0 +1,53 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # Virtual environments
28
+ venv/
29
+ ENV/
30
+ env/
31
+ .venv/
32
+
33
+ # pytest
34
+ .pytest_cache/
35
+ .coverage
36
+ htmlcov/
37
+
38
+ # mypy
39
+ .mypy_cache/
40
+
41
+ # IDE
42
+ .idea/
43
+ .vscode/
44
+ *.swp
45
+ *.swo
46
+
47
+ # OS
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Local development
52
+ .env
53
+ .env.local
mbuzz-0.1.0/PKG-INFO ADDED
@@ -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.
mbuzz-0.1.0/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # mbuzz-python
2
+
3
+ Python SDK for mbuzz multi-touch attribution.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install mbuzz
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import mbuzz
15
+
16
+ # Initialize (once on app start)
17
+ mbuzz.init(api_key="sk_live_...")
18
+
19
+ # Track events
20
+ mbuzz.event("page_view", url="/pricing")
21
+
22
+ # Track conversions
23
+ mbuzz.conversion("purchase", revenue=99.99)
24
+
25
+ # Identify users
26
+ mbuzz.identify("user_123", traits={"email": "user@example.com"})
27
+ ```
28
+
29
+ ## Documentation
30
+
31
+ See [mbuzz.co/docs/getting-started](https://mbuzz.co/docs/getting-started) for full documentation.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mbuzz"
7
+ version = "0.1.0"
8
+ description = "Multi-touch attribution SDK for Python"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "Mbuzz", email = "support@mbuzz.co" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Framework :: Django",
18
+ "Framework :: Flask",
19
+ "Framework :: FastAPI",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ ]
29
+ keywords = ["analytics", "attribution", "marketing", "tracking"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://mbuzz.co"
33
+ Documentation = "https://mbuzz.co/docs/getting-started"
34
+ Repository = "https://github.com/mbuzzco/mbuzz-python"
35
+
36
+ [project.optional-dependencies]
37
+ django = []
38
+ flask = []
39
+ fastapi = ["starlette"]
40
+ dev = ["pytest", "pytest-cov", "black", "ruff", "mypy"]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/mbuzz"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ select = ["E", "F", "I", "N", "W"]
48
+
49
+ [tool.mypy]
50
+ python_version = "3.8"
51
+ strict = true
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
@@ -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
+ ]
@@ -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)
@@ -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)