businnect 0.0.2__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.
businnect/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import Businnect
2
+
3
+ __all__ = ["Businnect"]
businnect/client.py ADDED
@@ -0,0 +1,37 @@
1
+ from businnect.settings import DEFAULT_API_SERVER
2
+ from businnect.resources.micropost import MicropostResource
3
+ from businnect.resources.blog import BlogResource
4
+ import requests
5
+
6
+
7
+ class Businnect:
8
+ """
9
+ Cliente para a API Businnect.
10
+
11
+ Args:
12
+ api_token: Seu token de API. Gere em https://businnect.com/settings/developer
13
+ base_url: URL base da API. Padrão: servidor Businnect.
14
+
15
+ Example:
16
+ client = Businnect(api_token="seu_token_aqui")
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ api_token: str,
22
+ base_url: str = DEFAULT_API_SERVER,
23
+ ) -> None:
24
+ self.api_token = api_token
25
+ self.base_url = base_url
26
+ self._headers = {"USER-API-TOKEN": api_token}
27
+
28
+ self.micropost = MicropostResource(self)
29
+ self.blog = BlogResource(self)
30
+
31
+ def _request(self, method: str, path: str, **kwargs) -> requests.Response:
32
+ url = f"{self.base_url}{path}"
33
+ extra_headers = kwargs.pop("headers", {})
34
+ headers = {**self._headers, **extra_headers}
35
+ response = requests.request(method, url, headers=headers, **kwargs)
36
+ response.raise_for_status()
37
+ return response
File without changes
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Optional
3
+ from businnect.schemas.blog import BlogResponse, BlogListResponse
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import Businnect
7
+
8
+
9
+ class BlogResource:
10
+ def __init__(self, client: "Businnect"):
11
+ self._client = client
12
+
13
+ def client_get_admin_blog(self) -> Optional[BlogResponse]:
14
+ """
15
+ Retorna um artigo público do admin.
16
+
17
+ Returns:
18
+ BlogResponse ou None se não houver artigo.
19
+ """
20
+ response = self._client._request("GET", "/api/v1/client/admin/blog")
21
+ data = response.json()
22
+ if data is None:
23
+ return None
24
+ return BlogResponse(**data)
25
+
26
+ def client_list_admin_blogs(self) -> BlogListResponse:
27
+ """
28
+ Retorna a lista de artigos públicos do admin.
29
+
30
+ Returns:
31
+ BlogListResponse com list, total_list e total_pages.
32
+ """
33
+ response = self._client._request("GET", "/api/v1/client/admin/blog/list")
34
+ return BlogListResponse(**response.json())
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Optional
3
+ from requests_toolbelt.multipart.encoder import MultipartEncoder
4
+
5
+ from businnect.schemas.micropost import PostType
6
+ from businnect.schemas.common import CreatedWithPublicIdAndLinkResponse, VoteResponse
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import Businnect
10
+
11
+
12
+ class MicropostResource:
13
+ def __init__(self, client: "Businnect"):
14
+ self._client = client
15
+
16
+ def client_create_micropost(
17
+ self,
18
+ body: str,
19
+ title: Optional[str] = None,
20
+ post_type: PostType = PostType.TEXT,
21
+ community_name: Optional[str] = None,
22
+ parent_micropost_public_id: Optional[str] = None,
23
+ parent_micropost_comment_public_id: Optional[str] = None,
24
+ allow_comments: bool = True,
25
+ is_draft: bool = False,
26
+ file: Optional[tuple] = None,
27
+ ) -> CreatedWithPublicIdAndLinkResponse:
28
+ """
29
+ Cria um micropost (texto, mídia ou reply).
30
+
31
+ Args:
32
+ body: Conteúdo do post.
33
+ title: Obrigatório para posts originais.
34
+ post_type: "TEXT" ou "MEDIA". Padrão: TEXT.
35
+ community_name: Comunidade onde postar (opcional).
36
+ parent_micropost_public_id: Para replies a um post.
37
+ parent_micropost_comment_public_id: Para replies aninhados.
38
+ allow_comments: Se permite comentários. Padrão: True.
39
+ is_draft: Salvar como rascunho. Padrão: False.
40
+ file: Tuple (filename, file_object, mime_type) para posts MEDIA.
41
+
42
+ Returns:
43
+ CreatedWithPublicIdAndLinkResponse com public_id e link.
44
+ """
45
+ fields = {
46
+ "body": body,
47
+ "post_type": post_type,
48
+ "allow_comments": str(allow_comments).lower(),
49
+ "is_draft": str(is_draft).lower(),
50
+ }
51
+
52
+ if title:
53
+ fields["title"] = title
54
+ if community_name:
55
+ fields["community_name"] = community_name
56
+ if parent_micropost_public_id:
57
+ fields["parent_micropost_public_id"] = parent_micropost_public_id
58
+ if parent_micropost_comment_public_id:
59
+ fields["parent_micropost_comment_public_id"] = parent_micropost_comment_public_id
60
+ if file:
61
+ fields["file"] = file
62
+
63
+ m = MultipartEncoder(fields=fields)
64
+
65
+ response = self._client._request(
66
+ "POST",
67
+ "/api/v1/client/micropost",
68
+ data=m,
69
+ headers={"Content-Type": m.content_type},
70
+ )
71
+
72
+ return CreatedWithPublicIdAndLinkResponse(**response.json())
73
+
74
+ def client_delete_micropost(self, public_id: str) -> None:
75
+ """
76
+ Deleta um micropost pelo public_id.
77
+
78
+ Args:
79
+ public_id: ID público do post a ser deletado.
80
+
81
+ Returns:
82
+ None (204 No Content em caso de sucesso).
83
+ """
84
+ self._client._request(
85
+ "DELETE",
86
+ "/api/v1/client/micropost",
87
+ json={"public_id": public_id},
88
+ )
89
+
90
+ def client_vote_micropost(self, public_id: str) -> VoteResponse:
91
+ """
92
+ Vota ou remove o voto de um micropost (toggle).
93
+
94
+ Args:
95
+ public_id: ID público do post.
96
+
97
+ Returns:
98
+ VoteResponse com user_voted (bool).
99
+ """
100
+ response = self._client._request(
101
+ "PATCH",
102
+ "/api/v1/client/micropost/vote",
103
+ json={"public_id": public_id},
104
+ )
105
+ return VoteResponse(**response.json())
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+ from enum import Enum, unique
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel
5
+
6
+
7
+ @unique
8
+ class BlogCategory(str, Enum):
9
+ ANNOUNCEMENT = "ANNOUNCEMENT"
10
+
11
+
12
+ class BlogItem(BaseModel):
13
+ body: str
14
+ category: BlogCategory
15
+ cover: Optional[str] = None
16
+ is_featured: bool
17
+ published_at: str
18
+ slug: str
19
+ summary: Optional[str] = None
20
+ title: str
21
+
22
+
23
+ class BlogResponse(BaseModel):
24
+ body: str
25
+ category: BlogCategory
26
+ cover: Optional[str] = None
27
+ is_featured: bool
28
+ published_at: str
29
+ slug: str
30
+ summary: Optional[str] = None
31
+ title: str
32
+
33
+
34
+ class BlogListResponse(BaseModel):
35
+ list: List[BlogItem]
36
+ total_list: int
37
+ total_pages: int
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class CreatedWithPublicIdAndLinkResponse(BaseModel):
5
+ public_id: str
6
+ link: str
7
+
8
+
9
+ class VoteResponse(BaseModel):
10
+ user_voted: bool
@@ -0,0 +1,6 @@
1
+ from enum import Enum, unique
2
+
3
+ @unique
4
+ class PostType(str, Enum):
5
+ TEXT = "TEXT"
6
+ MEDIA = "MEDIA"
businnect/settings.py ADDED
@@ -0,0 +1 @@
1
+ DEFAULT_API_SERVER = "https://api.businnect.com"
@@ -0,0 +1,24 @@
1
+ # Unit tests
2
+
3
+ import httpx
4
+ import pytest
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import sys
8
+ import os
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
10
+
11
+ from client import Businnect
12
+
13
+ # $pip install -e .
14
+ # $BUSINNECT_API_TOKEN=yours python -m pytest businnect/tests/test_businnect_integration.py -v -s
15
+ # @pytest.mark.skip(reason="already tested")
16
+ def test_client_create_micropost():
17
+ client = Businnect(api_token=os.environ["BUSINNECT_API_TOKEN"])
18
+ try:
19
+ response = client.micropost.client_create_micropost(title="Test", body="Hello")
20
+ assert response.public_id is not None
21
+ except Exception as e:
22
+ if hasattr(e, 'response'):
23
+ print(repr(e.response.text))
24
+ raise
@@ -0,0 +1,131 @@
1
+ # Testes unitários
2
+ # pip install -e .
3
+ # pytest businnect/tests/test_businnect_unit.py -v
4
+
5
+ import pytest
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import sys
9
+ import os
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
11
+
12
+ from client import Businnect
13
+ from settings import DEFAULT_API_SERVER
14
+
15
+
16
+ @pytest.fixture
17
+ def client():
18
+ return Businnect(api_token="test_token")
19
+
20
+
21
+ # --- Client ---
22
+
23
+ def test_init(client):
24
+ assert client.api_token == "test_token"
25
+ assert client._headers == {"USER-API-TOKEN": "test_token"}
26
+ assert client.base_url == DEFAULT_API_SERVER
27
+
28
+
29
+ @patch("businnect.client.requests.request")
30
+ def test_request_success(mock_request, client):
31
+ mock_response = MagicMock()
32
+ mock_response.status_code = 200
33
+ mock_request.return_value = mock_response
34
+
35
+ response = client._request("GET", "/some/path")
36
+
37
+ mock_request.assert_called_once_with(
38
+ "GET",
39
+ f"{DEFAULT_API_SERVER}/some/path",
40
+ headers={"USER-API-TOKEN": "test_token"},
41
+ )
42
+ assert response == mock_response
43
+
44
+
45
+ @patch("businnect.client.requests.request")
46
+ def test_request_raises_on_error(mock_request, client):
47
+ import requests as req
48
+ mock_response = MagicMock()
49
+ mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("404")
50
+ mock_request.return_value = mock_response
51
+
52
+ with pytest.raises(req.exceptions.HTTPError):
53
+ client._request("GET", "/bad/path")
54
+
55
+
56
+ # --- Micropost ---
57
+
58
+ @patch("businnect.resources.micropost.MultipartEncoder")
59
+ def test_create_micropost(mock_encoder, client):
60
+ mock_m = MagicMock()
61
+ mock_m.content_type = "multipart/form-data; boundary=abc"
62
+ mock_encoder.return_value = mock_m
63
+
64
+ mock_response = MagicMock()
65
+ mock_response.json.return_value = {"public_id": "abc123", "link": "https://businnect.com/p/abc123"}
66
+ client._request = MagicMock(return_value=mock_response)
67
+
68
+ result = client.micropost.client_create_micropost(title="Teste", body="Olá mundo")
69
+
70
+ assert result.public_id == "abc123"
71
+ assert result.link == "https://businnect.com/p/abc123"
72
+ client._request.assert_called_once()
73
+
74
+
75
+ def test_delete_micropost(client):
76
+ client._request = MagicMock()
77
+ client.micropost.client_delete_micropost(public_id="abc123")
78
+ client._request.assert_called_once_with(
79
+ "DELETE",
80
+ "/api/v1/client/micropost",
81
+ json={"public_id": "abc123"},
82
+ )
83
+
84
+
85
+ def test_vote_micropost(client):
86
+ mock_response = MagicMock()
87
+ mock_response.json.return_value = {"user_voted": True}
88
+ client._request = MagicMock(return_value=mock_response)
89
+
90
+ result = client.micropost.client_vote_micropost(public_id="abc123")
91
+
92
+ assert result.user_voted is True
93
+ client._request.assert_called_once_with(
94
+ "PATCH",
95
+ "/api/v1/client/micropost/vote",
96
+ json={"public_id": "abc123"},
97
+ )
98
+
99
+
100
+ # --- Blog ---
101
+
102
+ def test_get_admin_blog_none(client):
103
+ mock_response = MagicMock()
104
+ mock_response.json.return_value = None
105
+ client._request = MagicMock(return_value=mock_response)
106
+
107
+ result = client.blog.client_get_admin_blog()
108
+ assert result is None
109
+
110
+
111
+ def test_list_admin_blogs(client):
112
+ mock_response = MagicMock()
113
+ mock_response.json.return_value = {
114
+ "list": [
115
+ {
116
+ "body": "Conteúdo",
117
+ "category": "ANNOUNCEMENT",
118
+ "is_featured": True,
119
+ "published_at": "2026-03-25T16:17:12.799Z",
120
+ "slug": "meu-artigo",
121
+ "title": "Meu Artigo",
122
+ }
123
+ ],
124
+ "total_list": 1,
125
+ "total_pages": 1,
126
+ }
127
+ client._request = MagicMock(return_value=mock_response)
128
+
129
+ result = client.blog.client_list_admin_blogs()
130
+ assert result.total_list == 1
131
+ assert result.list[0].slug == "meu-artigo"
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: businnect
3
+ Version: 0.0.2
4
+ Summary: Python SDK for the Businnect API
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: requests>=2.32.0
9
+ Requires-Dist: requests-toolbelt>=1.0.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Dynamic: license-file
12
+
13
+ # Businnect Python
14
+
15
+ The official Python SDK for the [Businnect](https://businnect.com) API.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install businnect
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ Find your API token at [https://businnect.com/settings/developer](https://businnect.com/settings/developer) after creating an account at [https://businnect.com/register](https://businnect.com/register).
26
+
27
+ ```python
28
+ from businnect import Businnect
29
+
30
+ client = Businnect(api_token="your_api_token_here")
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Micropost
36
+
37
+ ### Create a post
38
+
39
+ ```python
40
+ from businnect.schemas.micropost import PostType
41
+
42
+ # Text post
43
+ post = client.micropost.client_create_micropost(
44
+ title="Hello World",
45
+ body="This is my first post.",
46
+ )
47
+ print(post.public_id)
48
+ print(post.link)
49
+
50
+ # Media post
51
+ with open("photo.png", "rb") as f:
52
+ post = client.micropost.client_create_micropost(
53
+ title="My photo",
54
+ body="Check this out",
55
+ post_type=PostType.MEDIA,
56
+ file=("photo.png", f, "image/png"),
57
+ )
58
+
59
+ # Reply to a post
60
+ reply = client.micropost.client_create_micropost(
61
+ body="Great post!",
62
+ parent_micropost_public_id="post_public_id_here",
63
+ )
64
+
65
+ # Nested reply (reply to a comment)
66
+ nested = client.micropost.client_create_micropost(
67
+ body="I agree!",
68
+ parent_micropost_public_id="post_public_id_here",
69
+ parent_micropost_comment_public_id="comment_public_id_here",
70
+ )
71
+ ```
72
+
73
+ ### Delete a post
74
+
75
+ ```python
76
+ client.micropost.client_delete_micropost(public_id="post_public_id_here")
77
+ ```
78
+
79
+ ### Vote on a post (toggle)
80
+
81
+ ```python
82
+ result = client.micropost.client_vote_micropost(public_id="post_public_id_here")
83
+ print(result.user_voted) # True or False
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Blog
89
+
90
+ ### Get admin article
91
+
92
+ ```python
93
+ article = client.blog.client_get_admin_blog()
94
+ if article:
95
+ print(article.title)
96
+ print(article.body)
97
+ ```
98
+
99
+ ### List admin articles
100
+
101
+ ```python
102
+ result = client.blog.client_list_admin_blogs()
103
+ print(result.total_list)
104
+ for article in result.list:
105
+ print(article.title, article.slug)
106
+ ```
107
+
108
+ ---
109
+
110
+ ## API Reference
111
+
112
+ ### `Businnect(api_token, base_url?)`
113
+
114
+ | Parameter | Type | Description |
115
+ |-----------|------|-------------|
116
+ | `api_token` | `str` | Your API token |
117
+ | `base_url` | `str` | API base URL (default: `https://api.businnect.com`) |
118
+
119
+ ---
120
+
121
+ ### `client.micropost.client_create_micropost(...)`
122
+
123
+ | Parameter | Type | Default | Description |
124
+ |-----------|------|---------|-------------|
125
+ | `body` | `str` | required | Content of the post |
126
+ | `title` | `str` | `None` | Required for original posts |
127
+ | `post_type` | `PostType` | `PostType.TEXT` | `TEXT` or `MEDIA` |
128
+ | `community_name` | `str` | `None` | Community to post in |
129
+ | `parent_micropost_public_id` | `str` | `None` | For replies |
130
+ | `parent_micropost_comment_public_id` | `str` | `None` | For nested replies |
131
+ | `allow_comments` | `bool` | `True` | Whether comments are allowed |
132
+ | `is_draft` | `bool` | `False` | Save as draft |
133
+ | `file` | `tuple` | `None` | `(filename, file_object, mime_type)` for MEDIA posts |
134
+
135
+ **Returns:** `CreatedWithPublicIdAndLinkResponse` with `public_id` and `link`
136
+
137
+ ---
138
+
139
+ ### `client.micropost.client_delete_micropost(public_id)`
140
+
141
+ | Parameter | Type | Description |
142
+ |-----------|------|-------------|
143
+ | `public_id` | `str` | Public ID of the post to delete |
144
+
145
+ **Returns:** `None` (204 on success)
146
+
147
+ ---
148
+
149
+ ### `client.micropost.client_vote_micropost(public_id)`
150
+
151
+ | Parameter | Type | Description |
152
+ |-----------|------|-------------|
153
+ | `public_id` | `str` | Public ID of the post to vote on |
154
+
155
+ **Returns:** `VoteResponse` with `user_voted` (bool)
156
+
157
+ ---
158
+
159
+ ### `client.blog.client_get_admin_blog()`
160
+
161
+ **Returns:** `BlogResponse` or `None`
162
+
163
+ ---
164
+
165
+ ### `client.blog.client_list_admin_blogs()`
166
+
167
+ **Returns:** `BlogListResponse` with `list`, `total_list` and `total_pages`
168
+
169
+ ---
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ # Install in editable mode
175
+ pip install -e .
176
+
177
+ # Run unit tests
178
+ python -m pytest businnect/tests/test_businnect_unit.py -v
179
+
180
+ # Run integration tests (requires real API token)
181
+ BUSINNECT_API_TOKEN=your_token python -m pytest businnect/tests/test_businnect_integration.py -v -s
182
+ ```
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,17 @@
1
+ businnect/__init__.py,sha256=Ug4w0IpjbQXyryYE3GZu-5_HF7MLoUGtntbT1EQTvf0,54
2
+ businnect/client.py,sha256=c45HQSQQ3kg5Dccc5P1XKZZZYrVwG8tPhG_zFJHBHjU,1160
3
+ businnect/settings.py,sha256=A1PRJGGib2fW3yJLfpHblWyhOOCgc9OhgGHPckTo7vk,48
4
+ businnect/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ businnect/resources/blog.py,sha256=eieZsQBFcddVXZgQzlEhNNIq4swgPdvNl-ZC3izya2o,1050
6
+ businnect/resources/micropost.py,sha256=RB0800Vv18Tqr3axDJJ-ynJWaR7zI_lJi3rf72LQQAM,3487
7
+ businnect/schemas/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ businnect/schemas/blog.py,sha256=-9SVreOyFAXA2NSAjgTv5YGSqZn5ZeRq3yF-l-qKRpc,730
9
+ businnect/schemas/common.py,sha256=m9Sm5-aDHqS1xsLBbCcTun8qeaU1MjphHwU9n7ht8FA,173
10
+ businnect/schemas/micropost.py,sha256=X-NwhGlm5zwk2YLqQ3nv7FlPXiTw1SNDuF1GuPyTriE,103
11
+ businnect/tests/test_businnect_integration.py,sha256=Kf7d_fv8LehPFLRxHhMADrkzYQVB2cha9KFlGqug0Rc,729
12
+ businnect/tests/test_businnect_unit.py,sha256=Dagb8jifCsoIMmWmETx703YmreiVKoRnZJ_O4bb-1bI,3763
13
+ businnect-0.0.2.dist-info/licenses/LICENSE,sha256=RwnK9-0uTiDG9cSNvgDvmgheQ38zQ34oJBwJyno922s,1113
14
+ businnect-0.0.2.dist-info/METADATA,sha256=bHOZTTZObdV00RgQkcyuy0PKuHbxa7J4zn1EUKqtZDQ,4344
15
+ businnect-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ businnect-0.0.2.dist-info/top_level.txt,sha256=NM9952km5ZEjn7pKTUrdAg4YOPFFihwx1dmsUTefLXU,10
17
+ businnect-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Businnect https://www.businnect.com support@businnect.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ businnect