Skip to content

Commit a04b7ac

Browse files
author
Peng Ren
committed
Added something new
1 parent 531e8ff commit a04b7ac

File tree

15 files changed

+2163
-58
lines changed

15 files changed

+2163
-58
lines changed

.github/workflows/ci.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: CI Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
15+
16+
services:
17+
mongodb:
18+
image: mongo:8.0
19+
env:
20+
MONGO_INITDB_ROOT_USERNAME: admin
21+
MONGO_INITDB_ROOT_PASSWORD: secret
22+
ports:
23+
- 27017:27017
24+
options: >-
25+
--health-cmd "mongosh --eval 'db.runCommand({ping: 1})' --quiet"
26+
--health-interval 30s
27+
--health-timeout 10s
28+
--health-retries 5
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Set up Python ${{ matrix.python-version }}
34+
uses: actions/setup-python@v4
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
38+
- name: Cache pip dependencies
39+
uses: actions/cache@v3
40+
with:
41+
path: ~/.cache/pip
42+
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements-test.txt') }}
43+
restore-keys: |
44+
${{ runner.os }}-py${{ matrix.python-version }}-pip-
45+
46+
- name: Install MongoDB shell
47+
run: |
48+
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
49+
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
50+
sudo apt-get update
51+
sudo apt-get install -y mongodb-mongosh
52+
53+
- name: Install dependencies
54+
run: |
55+
python -m pip install --upgrade pip
56+
pip install -r requirements-test.txt
57+
58+
- name: Wait for MongoDB to be ready
59+
run: |
60+
echo "Waiting for MongoDB to be ready..."
61+
for i in {1..30}; do
62+
if mongosh --host localhost:27017 --username admin --password secret --authenticationDatabase admin --eval "db.runCommand({ping: 1})" --quiet; then
63+
echo "MongoDB is ready!"
64+
break
65+
fi
66+
echo "Attempt $i: MongoDB not ready yet, waiting..."
67+
sleep 2
68+
done
69+
70+
- name: Set up test database
71+
run: |
72+
echo "Setting up test database..."
73+
python tests/mongo_test_helper.py setup || true
74+
75+
- name: Run tests
76+
run: |
77+
python -m pytest tests/ -v --tb=short
78+
79+
- name: Run tests with coverage
80+
run: |
81+
python -m pytest tests/ --cov=pymongosql --cov-report=term-missing --cov-report=xml
82+
83+
- name: Upload coverage reports
84+
if: github.event_name == 'push' && secrets.CODECOV_TOKEN
85+
uses: codecov/codecov-action@v3
86+
with:
87+
token: ${{ secrets.CODECOV_TOKEN }}
88+
file: ./coverage.xml
89+
fail_ci_if_error: false

25

Whitespace-only changes.

pymongosql/common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def arraysize(self) -> int:
7979

8080
@arraysize.setter
8181
def arraysize(self, value: int) -> None:
82-
if value <= 0 or value > self.DEFAULT_FETCH_SIZE:
82+
if value <= 0:
83+
raise ValueError("arraysize must be positive")
84+
if value > self.DEFAULT_FETCH_SIZE:
8385
raise ProgrammingError(
8486
f"MaxResults is more than maximum allowed length {self.DEFAULT_FETCH_SIZE}."
8587
)

pymongosql/connection.py

Lines changed: 215 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,150 @@
11
# -*- coding: utf-8 -*-
22
import logging
3-
from typing import Optional, Type
3+
from typing import Optional, Type, Dict, Any, Union
4+
from urllib.parse import quote_plus
45

5-
from .error import NotSupportedError
6+
from pymongo import MongoClient
7+
from pymongo.database import Database
8+
from pymongo.collection import Collection
9+
from pymongo.errors import PyMongoError, ConnectionFailure
10+
11+
from .error import NotSupportedError, DatabaseError, OperationalError
612
from .common import BaseCursor
13+
from .cursor import Cursor
714

815
_logger = logging.getLogger(__name__)
916

1017

1118
class Connection:
19+
"""MongoDB connection wrapper that provides SQL-like interface"""
1220

1321
def __init__(
1422
self,
23+
host: str,
24+
port: int,
25+
database: str = None,
26+
username: str = None,
27+
password: str = None,
28+
auth_source: str = None,
29+
ssl: bool = None,
30+
ssl_cert_reqs: str = None,
31+
connection_timeout: int = None,
32+
server_selection_timeout: int = None,
1533
**kwargs,
16-
) -> None: ...
34+
) -> None:
35+
"""Initialize MongoDB connection
36+
37+
Args:
38+
host: MongoDB host (required)
39+
port: MongoDB port (required)
40+
database: Default database name (optional)
41+
username: Username for authentication (optional)
42+
password: Password for authentication (optional)
43+
auth_source: Authentication database (optional)
44+
ssl: Enable SSL (optional)
45+
ssl_cert_reqs: SSL certificate requirements (optional)
46+
connection_timeout: Connection timeout in ms (optional)
47+
server_selection_timeout: Server selection timeout in ms (optional)
48+
**kwargs: Additional PyMongo connection parameters
49+
"""
50+
self._host = host
51+
self._port = port
52+
self._database_name = database
53+
self._username = username
54+
self._password = password
55+
self._auth_source = auth_source
56+
self._ssl = ssl
57+
self._ssl_cert_reqs = ssl_cert_reqs
58+
self._connection_timeout = connection_timeout
59+
self._server_selection_timeout = server_selection_timeout
60+
61+
self._autocommit = True
62+
self._in_transaction = False
63+
self._client: Optional[MongoClient] = None
64+
self._database: Optional[Database] = None
65+
self.cursor_pool = []
66+
self.cursor_class = Cursor
67+
self.cursor_kwargs = kwargs
68+
69+
# Establish connection
70+
self._connect()
71+
72+
def _connect(self) -> None:
73+
"""Establish connection to MongoDB"""
74+
try:
75+
# Build connection string
76+
if self._username and self._password:
77+
auth_string = (
78+
f"{quote_plus(self._username)}:{quote_plus(self._password)}@"
79+
)
80+
else:
81+
auth_string = ""
82+
83+
connection_string = f"mongodb://{auth_string}{self._host}:{self._port}/"
84+
85+
# Connection options with defaults only when needed
86+
options = {}
87+
88+
if self._connection_timeout is not None:
89+
options["connectTimeoutMS"] = self._connection_timeout
90+
if self._server_selection_timeout is not None:
91+
options["serverSelectionTimeoutMS"] = self._server_selection_timeout
92+
if self._ssl is not None:
93+
options["ssl"] = self._ssl
94+
if self._ssl_cert_reqs is not None:
95+
options["ssl_cert_reqs"] = self._ssl_cert_reqs
96+
97+
if self._username and self._auth_source:
98+
options["authSource"] = self._auth_source
99+
100+
# Create client
101+
self._client = MongoClient(connection_string, **options)
102+
103+
# Test connection
104+
self._client.admin.command("ping")
105+
106+
# Set database if specified
107+
if self._database_name:
108+
self._database = self._client[self._database_name]
109+
110+
_logger.info(
111+
f"Successfully connected to MongoDB at {self._host}:{self._port}"
112+
)
113+
114+
except ConnectionFailure as e:
115+
_logger.error(f"Failed to connect to MongoDB: {e}")
116+
raise OperationalError(f"Could not connect to MongoDB: {e}")
117+
except Exception as e:
118+
_logger.error(f"Unexpected error during connection: {e}")
119+
raise DatabaseError(f"Database connection error: {e}")
120+
121+
@property
122+
def client(self) -> MongoClient:
123+
"""Get the PyMongo client"""
124+
if self._client is None:
125+
raise OperationalError("No active connection")
126+
return self._client
127+
128+
@property
129+
def database(self) -> Database:
130+
"""Get the current database"""
131+
if self._database is None:
132+
raise OperationalError("No database selected")
133+
return self._database
134+
135+
def use_database(self, database_name: str) -> None:
136+
"""Switch to a different database"""
137+
if self._client is None:
138+
raise OperationalError("No active connection")
139+
self._database_name = database_name
140+
self._database = self._client[database_name]
141+
_logger.info(f"Switched to database: {database_name}")
142+
143+
def get_collection(self, collection_name: str) -> Collection:
144+
"""Get a collection from the current database"""
145+
if self._database is None:
146+
raise OperationalError("No database selected")
147+
return self._database[collection_name]
17148

18149
@property
19150
def autocommit(self) -> bool:
@@ -37,31 +168,107 @@ def in_transaction(self) -> bool:
37168
def in_transaction(self, value: bool) -> bool:
38169
self._in_transaction = False
39170

171+
@property
172+
def host(self) -> str:
173+
"""Get the hostname"""
174+
return self._host
175+
176+
@property
177+
def port(self) -> int:
178+
"""Get the port number"""
179+
return self._port
180+
181+
@property
182+
def database_name(self) -> str:
183+
"""Get the database name"""
184+
return self._database_name
185+
186+
@property
187+
def username(self) -> str:
188+
"""Get the username"""
189+
return self._username
190+
191+
@property
192+
def password(self) -> str:
193+
"""Get the password"""
194+
return self._password
195+
40196
def __enter__(self):
41197
return self
42198

43199
def __exit__(self, exc_type, exc_val, exc_tb):
44200
self.close()
45201

202+
@property
203+
def is_connected(self) -> bool:
204+
"""Check if connected to MongoDB"""
205+
return self._client is not None
206+
207+
@property
208+
def database_instance(self):
209+
"""Get the database instance"""
210+
return self._database
211+
212+
def disconnect(self) -> None:
213+
"""Disconnect from MongoDB (alias for close)"""
214+
self.close()
215+
216+
def __str__(self) -> str:
217+
"""String representation of the connection"""
218+
status = "connected" if self.is_connected else "disconnected"
219+
return f"Connection(host={self._host}, port={self._port}, database={self._database_name}, status={status})"
220+
46221
def cursor(self, cursor: Optional[Type[BaseCursor]] = None, **kwargs) -> BaseCursor:
47222
kwargs.update(self.cursor_kwargs)
48223
if not cursor:
49224
cursor = self.cursor_class
50225

51-
return cursor(
226+
new_cursor = cursor(
52227
connection=self,
53228
**kwargs,
54229
)
230+
self.cursor_pool.append(new_cursor)
231+
return new_cursor
55232

56-
def close(self) -> None: ...
233+
def close(self) -> None:
234+
"""Close the MongoDB connection"""
235+
try:
236+
# Close all cursors
237+
for cursor in self.cursor_pool:
238+
cursor.close()
239+
self.cursor_pool.clear()
240+
241+
# Close client connection
242+
if self._client:
243+
self._client.close()
244+
self._client = None
245+
self._database = None
246+
247+
_logger.info("MongoDB connection closed")
248+
except Exception as e:
249+
_logger.error(f"Error closing connection: {e}")
57250

58251
def begin(self) -> None:
59252
self._autocommit = False
60253
self._in_transaction = True
61254

62-
def commit(self) -> None: ...
255+
def commit(self) -> None:
256+
"""Commit transaction (MongoDB doesn't support traditional transactions in the same way)"""
257+
self._in_transaction = False
258+
self._autocommit = True
63259

64260
def rollback(self) -> None:
65-
raise NotSupportedError
261+
raise NotSupportedError(
262+
"MongoDB doesn't support rollback in the traditional SQL sense"
263+
)
66264

67-
def test_connection(self) -> bool: ...
265+
def test_connection(self) -> bool:
266+
"""Test if the connection is alive"""
267+
try:
268+
if self._client:
269+
self._client.admin.command("ping")
270+
return True
271+
return False
272+
except Exception as e:
273+
_logger.error(f"Connection test failed: {e}")
274+
return False

0 commit comments

Comments
 (0)