ownerrez-wrapper 0.1.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- ownerrez_wrapper-0.1.0/LICENSE +21 -0
- ownerrez_wrapper-0.1.0/MANIFEST.in +5 -0
- ownerrez_wrapper-0.1.0/PKG-INFO +187 -0
- ownerrez_wrapper-0.1.0/README.md +147 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/__init__.py +15 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/api.py +84 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/cli.py +114 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/constants.py +1 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/exceptions.py +3 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/model.py +218 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/restAdapter.py +76 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/PKG-INFO +187 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/SOURCES.txt +21 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/dependency_links.txt +1 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/entry_points.txt +2 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/requires.txt +12 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/top_level.txt +2 -0
- ownerrez_wrapper-0.1.0/requirements-dev.txt +9 -0
- ownerrez_wrapper-0.1.0/requirements.txt +2 -0
- ownerrez_wrapper-0.1.0/setup.cfg +4 -0
- ownerrez_wrapper-0.1.0/setup.py +48 -0
- ownerrez_wrapper-0.1.0/tests/__init__.py +0 -0
- ownerrez_wrapper-0.1.0/tests/test_api.py +124 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Geody Moore
|
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,187 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: ownerrez-wrapper
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python wrapper for the OwnerRez API
|
5
|
+
Home-page: https://github.com/gmoorevt/ownerrez-wrapper
|
6
|
+
Author: Geody Moore
|
7
|
+
Author-email: geody.moore@gmail.com
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Developers
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Requires-Python: >=3.10
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
License-File: LICENSE
|
19
|
+
Requires-Dist: requests>=2.25.0
|
20
|
+
Requires-Dist: python-dateutil>=2.8.0
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
24
|
+
Requires-Dist: pytest-cov>=2.0.0; extra == "dev"
|
25
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
26
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
27
|
+
Requires-Dist: flake8>=3.9.0; extra == "dev"
|
28
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
29
|
+
Requires-Dist: freezegun>=1.2.0; extra == "dev"
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: description
|
34
|
+
Dynamic: description-content-type
|
35
|
+
Dynamic: home-page
|
36
|
+
Dynamic: provides-extra
|
37
|
+
Dynamic: requires-dist
|
38
|
+
Dynamic: requires-python
|
39
|
+
Dynamic: summary
|
40
|
+
|
41
|
+
# OwnerRez API Wrapper
|
42
|
+
|
43
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
- Python 3.10 or higher
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
```bash
|
52
|
+
pip install ownerrez-wrapper
|
53
|
+
```
|
54
|
+
|
55
|
+
## Python Usage
|
56
|
+
|
57
|
+
```python
|
58
|
+
from ownerrez_wrapper import API
|
59
|
+
from datetime import datetime
|
60
|
+
|
61
|
+
# Initialize the API client
|
62
|
+
api = API(username="your_username", token="your_token")
|
63
|
+
|
64
|
+
# Get all properties
|
65
|
+
properties = api.getproperties()
|
66
|
+
for prop in properties:
|
67
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
68
|
+
|
69
|
+
# Get bookings for a property
|
70
|
+
bookings = api.getbookings(
|
71
|
+
property_id=123,
|
72
|
+
since_utc=datetime(2024, 1, 1)
|
73
|
+
)
|
74
|
+
for booking in bookings:
|
75
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
76
|
+
|
77
|
+
# Get a specific booking
|
78
|
+
booking = api.getbooking(booking_id=456)
|
79
|
+
print(f"Guest ID: {booking.guest_id}")
|
80
|
+
|
81
|
+
# Get guest details
|
82
|
+
guest = api.getguest(guest_id=789)
|
83
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
84
|
+
|
85
|
+
# Check if a property is currently booked
|
86
|
+
is_booked = api.isunitbooked(property_id=123)
|
87
|
+
print(f"Property is booked: {is_booked}")
|
88
|
+
```
|
89
|
+
|
90
|
+
## API Documentation
|
91
|
+
|
92
|
+
### API Class Methods
|
93
|
+
|
94
|
+
- `getproperties()` -> List[Property]
|
95
|
+
- Returns a list of all properties
|
96
|
+
|
97
|
+
- `getproperty(property_id: int)` -> Property
|
98
|
+
- Get details for a specific property
|
99
|
+
- Parameters:
|
100
|
+
- property_id: The property ID
|
101
|
+
|
102
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
103
|
+
- Get bookings for a specific property since a given date
|
104
|
+
- Parameters:
|
105
|
+
- property_id: The property ID
|
106
|
+
- since_utc: DateTime object for the start date
|
107
|
+
|
108
|
+
- `getbooking(booking_id: int)` -> Booking
|
109
|
+
- Get details for a specific booking
|
110
|
+
- Parameters:
|
111
|
+
- booking_id: The booking ID
|
112
|
+
|
113
|
+
- `getguest(guest_id: int)` -> Guest
|
114
|
+
- Get details for a specific guest
|
115
|
+
- Parameters:
|
116
|
+
- guest_id: The guest ID
|
117
|
+
|
118
|
+
- `isunitbooked(property_id: int)` -> bool
|
119
|
+
- Check if a property is currently booked
|
120
|
+
- Parameters:
|
121
|
+
- property_id: The property ID
|
122
|
+
|
123
|
+
### Data Models
|
124
|
+
|
125
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
126
|
+
|
127
|
+
Main models:
|
128
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
129
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
130
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
131
|
+
|
132
|
+
Supporting models:
|
133
|
+
- `Address`: Physical address information
|
134
|
+
- `EmailAddress`: Email contact information
|
135
|
+
- `Phone`: Phone contact information
|
136
|
+
- `Charge`: Booking charge information
|
137
|
+
- `DoorCode`: Property access codes
|
138
|
+
|
139
|
+
## CLI Tool (for testing)
|
140
|
+
|
141
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
# Using .env file
|
145
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
146
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
147
|
+
|
148
|
+
# List properties
|
149
|
+
ownerrez properties
|
150
|
+
|
151
|
+
# Get property details
|
152
|
+
ownerrez property 123
|
153
|
+
|
154
|
+
# Check bookings
|
155
|
+
ownerrez bookings 123 --since 2024-01-01
|
156
|
+
|
157
|
+
# Get booking details
|
158
|
+
ownerrez booking 456
|
159
|
+
```
|
160
|
+
|
161
|
+
## Development
|
162
|
+
|
163
|
+
To set up the development environment:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
167
|
+
cd ownerrez-wrapper
|
168
|
+
pip install -e ".[dev]"
|
169
|
+
```
|
170
|
+
|
171
|
+
To run tests:
|
172
|
+
|
173
|
+
```bash
|
174
|
+
pytest
|
175
|
+
```
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
180
|
+
|
181
|
+
## Author
|
182
|
+
|
183
|
+
- Geody Moore (geody.moore@gmail.com)
|
184
|
+
|
185
|
+
## Contributing
|
186
|
+
|
187
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# OwnerRez API Wrapper
|
2
|
+
|
3
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
- Python 3.10 or higher
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
```bash
|
12
|
+
pip install ownerrez-wrapper
|
13
|
+
```
|
14
|
+
|
15
|
+
## Python Usage
|
16
|
+
|
17
|
+
```python
|
18
|
+
from ownerrez_wrapper import API
|
19
|
+
from datetime import datetime
|
20
|
+
|
21
|
+
# Initialize the API client
|
22
|
+
api = API(username="your_username", token="your_token")
|
23
|
+
|
24
|
+
# Get all properties
|
25
|
+
properties = api.getproperties()
|
26
|
+
for prop in properties:
|
27
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
28
|
+
|
29
|
+
# Get bookings for a property
|
30
|
+
bookings = api.getbookings(
|
31
|
+
property_id=123,
|
32
|
+
since_utc=datetime(2024, 1, 1)
|
33
|
+
)
|
34
|
+
for booking in bookings:
|
35
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
36
|
+
|
37
|
+
# Get a specific booking
|
38
|
+
booking = api.getbooking(booking_id=456)
|
39
|
+
print(f"Guest ID: {booking.guest_id}")
|
40
|
+
|
41
|
+
# Get guest details
|
42
|
+
guest = api.getguest(guest_id=789)
|
43
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
44
|
+
|
45
|
+
# Check if a property is currently booked
|
46
|
+
is_booked = api.isunitbooked(property_id=123)
|
47
|
+
print(f"Property is booked: {is_booked}")
|
48
|
+
```
|
49
|
+
|
50
|
+
## API Documentation
|
51
|
+
|
52
|
+
### API Class Methods
|
53
|
+
|
54
|
+
- `getproperties()` -> List[Property]
|
55
|
+
- Returns a list of all properties
|
56
|
+
|
57
|
+
- `getproperty(property_id: int)` -> Property
|
58
|
+
- Get details for a specific property
|
59
|
+
- Parameters:
|
60
|
+
- property_id: The property ID
|
61
|
+
|
62
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
63
|
+
- Get bookings for a specific property since a given date
|
64
|
+
- Parameters:
|
65
|
+
- property_id: The property ID
|
66
|
+
- since_utc: DateTime object for the start date
|
67
|
+
|
68
|
+
- `getbooking(booking_id: int)` -> Booking
|
69
|
+
- Get details for a specific booking
|
70
|
+
- Parameters:
|
71
|
+
- booking_id: The booking ID
|
72
|
+
|
73
|
+
- `getguest(guest_id: int)` -> Guest
|
74
|
+
- Get details for a specific guest
|
75
|
+
- Parameters:
|
76
|
+
- guest_id: The guest ID
|
77
|
+
|
78
|
+
- `isunitbooked(property_id: int)` -> bool
|
79
|
+
- Check if a property is currently booked
|
80
|
+
- Parameters:
|
81
|
+
- property_id: The property ID
|
82
|
+
|
83
|
+
### Data Models
|
84
|
+
|
85
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
86
|
+
|
87
|
+
Main models:
|
88
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
89
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
90
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
91
|
+
|
92
|
+
Supporting models:
|
93
|
+
- `Address`: Physical address information
|
94
|
+
- `EmailAddress`: Email contact information
|
95
|
+
- `Phone`: Phone contact information
|
96
|
+
- `Charge`: Booking charge information
|
97
|
+
- `DoorCode`: Property access codes
|
98
|
+
|
99
|
+
## CLI Tool (for testing)
|
100
|
+
|
101
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
# Using .env file
|
105
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
106
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
107
|
+
|
108
|
+
# List properties
|
109
|
+
ownerrez properties
|
110
|
+
|
111
|
+
# Get property details
|
112
|
+
ownerrez property 123
|
113
|
+
|
114
|
+
# Check bookings
|
115
|
+
ownerrez bookings 123 --since 2024-01-01
|
116
|
+
|
117
|
+
# Get booking details
|
118
|
+
ownerrez booking 456
|
119
|
+
```
|
120
|
+
|
121
|
+
## Development
|
122
|
+
|
123
|
+
To set up the development environment:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
127
|
+
cd ownerrez-wrapper
|
128
|
+
pip install -e ".[dev]"
|
129
|
+
```
|
130
|
+
|
131
|
+
To run tests:
|
132
|
+
|
133
|
+
```bash
|
134
|
+
pytest
|
135
|
+
```
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
140
|
+
|
141
|
+
## Author
|
142
|
+
|
143
|
+
- Geody Moore (geody.moore@gmail.com)
|
144
|
+
|
145
|
+
## Contributing
|
146
|
+
|
147
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
OwnerRez API Wrapper
|
3
|
+
~~~~~~~~~~~~~~~~~~~
|
4
|
+
|
5
|
+
A Python wrapper for the OwnerRez API.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .api import API
|
9
|
+
from .cli import main as cli
|
10
|
+
|
11
|
+
__version__ = "0.1.0"
|
12
|
+
__author__ = "Geody Moore"
|
13
|
+
__email__ = "geody.moore@gmail.com"
|
14
|
+
|
15
|
+
__all__ = ["API", "cli"]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from typing import List
|
2
|
+
from datetime import datetime
|
3
|
+
from .restAdapter import RestAdapter
|
4
|
+
from .model import Property, Booking, Guest
|
5
|
+
|
6
|
+
|
7
|
+
class API(object):
|
8
|
+
"""
|
9
|
+
OwnerRez API wrapper class
|
10
|
+
"""
|
11
|
+
|
12
|
+
def __init__(self,username, token):
|
13
|
+
"""
|
14
|
+
Initialize the OwnerRez API wrapper with the OwnerRez username and token
|
15
|
+
:param username: OwnerRez username
|
16
|
+
:param token: OwnerRez token
|
17
|
+
"""
|
18
|
+
self.username = username
|
19
|
+
self.token = token
|
20
|
+
|
21
|
+
|
22
|
+
def getproperties(self) -> list:
|
23
|
+
"""
|
24
|
+
Get a list of properties.
|
25
|
+
"""
|
26
|
+
restAdapt = RestAdapter(self.username,self.token)
|
27
|
+
results = []
|
28
|
+
property_list = restAdapt.get(endpoint='properties')
|
29
|
+
for prop in property_list.data['items']:
|
30
|
+
prop = Property(**prop)
|
31
|
+
results.append(prop)
|
32
|
+
return results
|
33
|
+
|
34
|
+
def getproperty(self, property_id: int) -> Property:
|
35
|
+
"""
|
36
|
+
Get a single property by ID.
|
37
|
+
"""
|
38
|
+
restAdapt = RestAdapter(self.username,self.token)
|
39
|
+
property_data = restAdapt.get(endpoint=f'properties/{property_id}')
|
40
|
+
return Property(**property_data.data)
|
41
|
+
|
42
|
+
def getbookings(self, property_id: int, since_utc: datetime) -> List[Booking]:
|
43
|
+
"""
|
44
|
+
Get a list of bookings for a property since a given date.
|
45
|
+
"""
|
46
|
+
restAdapt = RestAdapter(self.username,self.token)
|
47
|
+
results = []
|
48
|
+
params = {'since_utc': since_utc, 'property_id': property_id}
|
49
|
+
booking_list = restAdapt.get(endpoint='bookings', ep_params=params)
|
50
|
+
|
51
|
+
for booking in booking_list.data['items']:
|
52
|
+
booking = Booking(**booking)
|
53
|
+
results.append(booking)
|
54
|
+
return results
|
55
|
+
|
56
|
+
def getbooking(self, booking_id: int) -> Booking:
|
57
|
+
"""
|
58
|
+
Get a single booking by ID.
|
59
|
+
"""
|
60
|
+
restAdapt = RestAdapter(self.username,self.token)
|
61
|
+
booking = restAdapt.get(endpoint=f'bookings/{booking_id}')
|
62
|
+
return Booking(**booking.data)
|
63
|
+
|
64
|
+
def getguest(self, guest_id: int) -> Guest:
|
65
|
+
"""
|
66
|
+
Get a single guest by ID.
|
67
|
+
"""
|
68
|
+
restAdapt = RestAdapter(self.username,self.token)
|
69
|
+
guest = restAdapt.get(endpoint=f'guests/{guest_id}')
|
70
|
+
return Guest(**guest.data)
|
71
|
+
|
72
|
+
def isunitbooked(self, property_id: int) -> bool:
|
73
|
+
"""
|
74
|
+
Check if a unit is booked today.
|
75
|
+
"""
|
76
|
+
today = datetime.today()
|
77
|
+
bookings = self.getbookings(property_id=property_id, since_utc=today)
|
78
|
+
for booking in bookings:
|
79
|
+
# Convert string dates to datetime if they aren't already
|
80
|
+
arrival = booking.arrival if isinstance(booking.arrival, datetime) else datetime.strptime(booking.arrival, "%Y-%m-%d")
|
81
|
+
departure = booking.departure if isinstance(booking.departure, datetime) else datetime.strptime(booking.departure, "%Y-%m-%d")
|
82
|
+
if arrival <= today and departure >= today:
|
83
|
+
return True
|
84
|
+
return False
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import argparse
|
3
|
+
from datetime import datetime
|
4
|
+
import json
|
5
|
+
from typing import Any
|
6
|
+
import os
|
7
|
+
from pathlib import Path
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
from .api import API
|
10
|
+
from uuid import UUID
|
11
|
+
|
12
|
+
def json_serial(obj: Any) -> str:
|
13
|
+
"""JSON serializer for objects not serializable by default json code"""
|
14
|
+
if isinstance(obj, datetime):
|
15
|
+
return obj.isoformat()
|
16
|
+
if isinstance(obj, UUID):
|
17
|
+
return str(obj)
|
18
|
+
if hasattr(obj, '__dict__'):
|
19
|
+
return {k: v for k, v in obj.__dict__.items() if v is not None}
|
20
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
21
|
+
|
22
|
+
def print_json(data: Any) -> None:
|
23
|
+
"""Print data as formatted JSON"""
|
24
|
+
print(json.dumps(data, indent=2, default=json_serial))
|
25
|
+
|
26
|
+
def load_env():
|
27
|
+
"""Load environment variables from .env file"""
|
28
|
+
env_path = Path('.env')
|
29
|
+
if env_path.exists():
|
30
|
+
load_dotenv(env_path)
|
31
|
+
|
32
|
+
def main():
|
33
|
+
# Load environment variables from .env file
|
34
|
+
load_env()
|
35
|
+
|
36
|
+
parser = argparse.ArgumentParser(description='OwnerRez API CLI')
|
37
|
+
|
38
|
+
# Authentication arguments
|
39
|
+
parser.add_argument('--username', help='OwnerRez API username')
|
40
|
+
parser.add_argument('--token', help='OwnerRez API token')
|
41
|
+
|
42
|
+
# Commands
|
43
|
+
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
44
|
+
|
45
|
+
# Properties command
|
46
|
+
properties_parser = subparsers.add_parser('properties', help='List all properties')
|
47
|
+
|
48
|
+
# Property command
|
49
|
+
property_parser = subparsers.add_parser('property', help='Get a specific property')
|
50
|
+
property_parser.add_argument('property_id', type=int, help='Property ID')
|
51
|
+
|
52
|
+
# Bookings command
|
53
|
+
bookings_parser = subparsers.add_parser('bookings', help='Get bookings for a property')
|
54
|
+
bookings_parser.add_argument('property_id', type=int, help='Property ID')
|
55
|
+
bookings_parser.add_argument('--since', type=str, help='Get bookings since date (YYYY-MM-DD)')
|
56
|
+
|
57
|
+
# Booking command
|
58
|
+
booking_parser = subparsers.add_parser('booking', help='Get a specific booking')
|
59
|
+
booking_parser.add_argument('booking_id', type=int, help='Booking ID')
|
60
|
+
|
61
|
+
# Guest command
|
62
|
+
guest_parser = subparsers.add_parser('guest', help='Get a specific guest')
|
63
|
+
guest_parser.add_argument('guest_id', type=int, help='Guest ID')
|
64
|
+
|
65
|
+
# Check if unit is booked command
|
66
|
+
booked_parser = subparsers.add_parser('is-booked', help='Check if a unit is currently booked')
|
67
|
+
booked_parser.add_argument('property_id', type=int, help='Property ID')
|
68
|
+
|
69
|
+
args = parser.parse_args()
|
70
|
+
|
71
|
+
# Get credentials from arguments or environment
|
72
|
+
username = args.username or os.environ.get('OWNERREZ_USERNAME')
|
73
|
+
token = args.token or os.environ.get('OWNERREZ_TOKEN')
|
74
|
+
|
75
|
+
if not username or not token:
|
76
|
+
parser.error('Username and token must be provided via arguments, environment variables, or .env file')
|
77
|
+
|
78
|
+
api = API(username=username, token=token)
|
79
|
+
|
80
|
+
try:
|
81
|
+
if args.command == 'properties':
|
82
|
+
properties = api.getproperties()
|
83
|
+
print_json(properties)
|
84
|
+
|
85
|
+
elif args.command == 'property':
|
86
|
+
property = api.getproperty(property_id=args.property_id)
|
87
|
+
print_json(property)
|
88
|
+
|
89
|
+
elif args.command == 'bookings':
|
90
|
+
since_date = datetime.strptime(args.since, "%Y-%m-%d") if args.since else datetime.today()
|
91
|
+
bookings = api.getbookings(property_id=args.property_id, since_utc=since_date)
|
92
|
+
print_json(bookings)
|
93
|
+
|
94
|
+
elif args.command == 'booking':
|
95
|
+
booking = api.getbooking(booking_id=args.booking_id)
|
96
|
+
print_json(booking)
|
97
|
+
|
98
|
+
elif args.command == 'guest':
|
99
|
+
guest = api.getguest(guest_id=args.guest_id)
|
100
|
+
print_json(guest)
|
101
|
+
|
102
|
+
elif args.command == 'is-booked':
|
103
|
+
is_booked = api.isunitbooked(property_id=args.property_id)
|
104
|
+
print_json({"is_booked": is_booked})
|
105
|
+
|
106
|
+
else:
|
107
|
+
parser.print_help()
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
print(f"Error: {str(e)}")
|
111
|
+
exit(1)
|
112
|
+
|
113
|
+
if __name__ == '__main__':
|
114
|
+
main()
|
@@ -0,0 +1 @@
|
|
1
|
+
BASEURL = "https://api.ownerreservations.com/v2"
|
@@ -0,0 +1,218 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from uuid import UUID
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import List, Dict, Optional, Any
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class Result:
|
9
|
+
message: str
|
10
|
+
status: str
|
11
|
+
data: Optional[List[Dict]] = None
|
12
|
+
|
13
|
+
def __init__(self, **kwargs):
|
14
|
+
for key, value in kwargs.items():
|
15
|
+
if hasattr(self, key):
|
16
|
+
setattr(self, key, value)
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Address:
|
20
|
+
city: Optional[str] = None
|
21
|
+
country: Optional[str] = None
|
22
|
+
id: Optional[int] = None
|
23
|
+
is_default: Optional[bool] = None
|
24
|
+
postal_code: Optional[str] = None
|
25
|
+
province: Optional[str] = None
|
26
|
+
state: Optional[str] = None
|
27
|
+
street1: Optional[str] = None
|
28
|
+
street2: Optional[str] = None
|
29
|
+
type: Optional[str] = None
|
30
|
+
|
31
|
+
def __init__(self, **kwargs):
|
32
|
+
for key, value in kwargs.items():
|
33
|
+
if hasattr(self, key):
|
34
|
+
setattr(self, key, value)
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class EmailAddress:
|
38
|
+
address: Optional[str] = None
|
39
|
+
id: Optional[int] = None
|
40
|
+
is_default: Optional[bool] = None
|
41
|
+
type: Optional[str] = None
|
42
|
+
|
43
|
+
def __init__(self, **kwargs):
|
44
|
+
for key, value in kwargs.items():
|
45
|
+
if hasattr(self, key):
|
46
|
+
setattr(self, key, value)
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class Phone:
|
50
|
+
extension: Optional[str] = None
|
51
|
+
id: Optional[int] = None
|
52
|
+
is_default: Optional[bool] = None
|
53
|
+
number: Optional[str] = None
|
54
|
+
type: Optional[str] = None
|
55
|
+
|
56
|
+
def __init__(self, **kwargs):
|
57
|
+
for key, value in kwargs.items():
|
58
|
+
if hasattr(self, key):
|
59
|
+
setattr(self, key, value)
|
60
|
+
|
61
|
+
@dataclass
|
62
|
+
class Guest:
|
63
|
+
addresses: Optional[List[Address]] = None
|
64
|
+
email_addresses: Optional[List[EmailAddress]] = None
|
65
|
+
first_name: Optional[str] = None
|
66
|
+
id: Optional[int] = None
|
67
|
+
last_name: Optional[str] = None
|
68
|
+
notes: Optional[str] = None
|
69
|
+
phones: Optional[List[Phone]] = None
|
70
|
+
|
71
|
+
def __init__(self, **kwargs):
|
72
|
+
for key, value in kwargs.items():
|
73
|
+
if hasattr(self, key):
|
74
|
+
if key == 'addresses' and value:
|
75
|
+
self.addresses = [Address(**addr) for addr in value]
|
76
|
+
elif key == 'email_addresses' and value:
|
77
|
+
self.email_addresses = [EmailAddress(**email) for email in value]
|
78
|
+
elif key == 'phones' and value:
|
79
|
+
self.phones = [Phone(**phone) for phone in value]
|
80
|
+
else:
|
81
|
+
setattr(self, key, value)
|
82
|
+
|
83
|
+
@dataclass
|
84
|
+
class Property:
|
85
|
+
active: Optional[bool] = None
|
86
|
+
address: Optional[Address] = None
|
87
|
+
bathrooms: Optional[int] = None
|
88
|
+
bathrooms_full: Optional[int] = None
|
89
|
+
bathrooms_half: Optional[int] = None
|
90
|
+
bedrooms: Optional[int] = None
|
91
|
+
check_in: Optional[str] = None
|
92
|
+
check_in_end: Optional[str] = None
|
93
|
+
check_out: Optional[str] = None
|
94
|
+
currency_code: Optional[str] = None
|
95
|
+
display_order: Optional[int] = None
|
96
|
+
external_display_order: Optional[int] = None
|
97
|
+
external_name: Optional[str] = None
|
98
|
+
id: Optional[int] = None
|
99
|
+
internal_code: Optional[str] = None
|
100
|
+
key: Optional[UUID] = None
|
101
|
+
latitude: Optional[int] = None
|
102
|
+
longitude: Optional[int] = None
|
103
|
+
max_adults: Optional[int] = None
|
104
|
+
max_children: Optional[int] = None
|
105
|
+
max_guests: Optional[int] = None
|
106
|
+
max_pets: Optional[int] = None
|
107
|
+
name: Optional[str] = None
|
108
|
+
owner_id: Optional[int] = None
|
109
|
+
property_type: Optional[str] = None
|
110
|
+
public_url: Optional[str] = None
|
111
|
+
thumbnail_url: Optional[str] = None
|
112
|
+
thumbnail_url_large: Optional[str] = None
|
113
|
+
thumbnail_url_medium: Optional[str] = None
|
114
|
+
living_area: Optional[int] = None
|
115
|
+
|
116
|
+
def __init__(self, **kwargs):
|
117
|
+
for key, value in kwargs.items():
|
118
|
+
if hasattr(self, key):
|
119
|
+
if key == 'address' and value:
|
120
|
+
self.address = Address(**value)
|
121
|
+
else:
|
122
|
+
setattr(self, key, value)
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
@dataclass
|
127
|
+
class Charge:
|
128
|
+
amount: Optional[int] = None
|
129
|
+
commission_amount: Optional[int] = None
|
130
|
+
description: Optional[str] = None
|
131
|
+
expense_id: Optional[int] = None
|
132
|
+
is_channel_managed: Optional[bool] = None
|
133
|
+
is_commission_all: Optional[bool] = None
|
134
|
+
is_expense_all: Optional[bool] = None
|
135
|
+
is_taxable: Optional[bool] = None
|
136
|
+
owner_amount: Optional[int] = None
|
137
|
+
owner_commission_percent: Optional[int] = None
|
138
|
+
position: Optional[int] = None
|
139
|
+
rate: Optional[int] = None
|
140
|
+
rate_is_percent: Optional[bool] = None
|
141
|
+
surcharge_id: Optional[int] = None
|
142
|
+
tax_id: Optional[int] = None
|
143
|
+
type: Optional[str] = None
|
144
|
+
|
145
|
+
def __init__(self, **kwargs):
|
146
|
+
for key, value in kwargs.items():
|
147
|
+
if hasattr(self, key):
|
148
|
+
setattr(self, key, value)
|
149
|
+
|
150
|
+
|
151
|
+
@dataclass
|
152
|
+
class DoorCode:
|
153
|
+
code: Optional[str] = None
|
154
|
+
lock_names: Optional[str] = None
|
155
|
+
|
156
|
+
def __init__(self, **kwargs):
|
157
|
+
for key, value in kwargs.items():
|
158
|
+
if hasattr(self, key):
|
159
|
+
setattr(self, key, value)
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
@dataclass
|
164
|
+
class Booking:
|
165
|
+
adults: Optional[int] = None
|
166
|
+
arrival: Optional[datetime] = None
|
167
|
+
booked_utc: Optional[datetime] = None
|
168
|
+
canceled_utc: Optional[datetime] = None
|
169
|
+
charges: Optional[List[Charge]] = None
|
170
|
+
check_in: Optional[str] = None
|
171
|
+
check_in_end: Optional[str] = None
|
172
|
+
check_out: Optional[str] = None
|
173
|
+
children: Optional[int] = None
|
174
|
+
cleaning_date: Optional[datetime] = None
|
175
|
+
currency_code: Optional[str] = None
|
176
|
+
departure: Optional[datetime] = None
|
177
|
+
door_codes: Optional[List[DoorCode]] = None
|
178
|
+
form_key: Optional[str] = None
|
179
|
+
guest: Optional[Guest] = None
|
180
|
+
guest_id: Optional[int] = None
|
181
|
+
id: Optional[int] = None
|
182
|
+
infants: Optional[int] = None
|
183
|
+
is_block: Optional[bool] = None
|
184
|
+
listing_site: Optional[str] = None
|
185
|
+
notes: Optional[str] = None
|
186
|
+
owner_id: Optional[int] = None
|
187
|
+
pending_until_utc: Optional[datetime] = None
|
188
|
+
pets: Optional[int] = None
|
189
|
+
platform_email_address: Optional[str] = None
|
190
|
+
platform_reservation_number: Optional[str] = None
|
191
|
+
property_id: Optional[int] = None
|
192
|
+
quote_id: Optional[int] = None
|
193
|
+
status: Optional[str] = None
|
194
|
+
title: Optional[str] = None
|
195
|
+
total_amount: Optional[int] = None
|
196
|
+
total_host_fees: Optional[int] = None
|
197
|
+
total_owed: Optional[int] = None
|
198
|
+
total_paid: Optional[int] = None
|
199
|
+
total_refunded: Optional[int] = None
|
200
|
+
type: Optional[str] = None
|
201
|
+
|
202
|
+
def __init__(self, **kwargs):
|
203
|
+
for key, value in kwargs.items():
|
204
|
+
if hasattr(self, key):
|
205
|
+
if key in ['arrival', 'departure', 'booked_utc', 'canceled_utc', 'cleaning_date', 'pending_until_utc'] and value:
|
206
|
+
# Convert string dates to datetime objects
|
207
|
+
try:
|
208
|
+
setattr(self, key, datetime.fromisoformat(value.replace('Z', '+00:00')))
|
209
|
+
except (ValueError, AttributeError):
|
210
|
+
setattr(self, key, value)
|
211
|
+
elif key == 'charges' and value:
|
212
|
+
self.charges = [Charge(**charge) for charge in value]
|
213
|
+
elif key == 'door_codes' and value:
|
214
|
+
self.door_codes = [DoorCode(**code) for code in value]
|
215
|
+
elif key == 'guest' and value:
|
216
|
+
self.guest = Guest(**value)
|
217
|
+
else:
|
218
|
+
setattr(self, key, value)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import requests
|
2
|
+
from json import JSONDecodeError
|
3
|
+
from ownerrez_wrapper.model import Result
|
4
|
+
import base64
|
5
|
+
from typing import List, Dict
|
6
|
+
import logging
|
7
|
+
import datetime
|
8
|
+
from ownerrez_wrapper.constants import BASEURL as hosturl
|
9
|
+
from ownerrez_wrapper.exceptions import OwnerrezApiException
|
10
|
+
|
11
|
+
class RestAdapter(object):
|
12
|
+
def __init__(self,username, token):
|
13
|
+
"""
|
14
|
+
Initialize the RestAdapter object with the OwnerRez username and token
|
15
|
+
:param username: OwnerRez username
|
16
|
+
:param token: OwnerRez token
|
17
|
+
"""
|
18
|
+
auth_string = f"{username}:{token}"
|
19
|
+
auth_bytes = auth_string.encode('ascii')
|
20
|
+
auth_b64 = base64.b64encode(auth_bytes).decode('ascii')
|
21
|
+
self.headers = {
|
22
|
+
'User-Agent': 'OwnerRezAPI',
|
23
|
+
'Content-Type': 'application/json',
|
24
|
+
'Authorization': f'Basic {auth_b64}'
|
25
|
+
}
|
26
|
+
self._logger = logging.getLogger(__name__)
|
27
|
+
self._logger.setLevel(logging.DEBUG)
|
28
|
+
|
29
|
+
def _do_request(self, http_method, endpoint, ep_params, data: Dict = None):
|
30
|
+
"""
|
31
|
+
Make a request to the OwnerRez API
|
32
|
+
:param http_method: HTTP method to use
|
33
|
+
:param endpoint: API endpoint to call
|
34
|
+
:param ep_params: Dictionary of parameters to pass to the endpoint
|
35
|
+
:param data: Dictionary of data to pass in the request body
|
36
|
+
:return: Result object
|
37
|
+
"""
|
38
|
+
|
39
|
+
full_url = f"{hosturl}/{endpoint.lstrip('/')}"
|
40
|
+
log_line_pre = f"method={http_method}, url={full_url}, params={ep_params}"
|
41
|
+
log_line_post = ', '.join((log_line_pre, "success={}, status_code={}, message={}"))
|
42
|
+
|
43
|
+
# Make the request
|
44
|
+
try:
|
45
|
+
self._logger.debug(msg = log_line_pre)
|
46
|
+
response = requests.request(method=http_method, url=full_url, headers=self.headers, params=ep_params, json=data)
|
47
|
+
|
48
|
+
# If the request fails, log the error and raise an exception
|
49
|
+
except requests.exceptions.RequestException as e:
|
50
|
+
self._logger.error(msg=(str(e)))
|
51
|
+
raise OwnerrezApiException("Request failed") from e
|
52
|
+
|
53
|
+
# Check status code first
|
54
|
+
is_success = 299 >= response.status_code >= 200 # 200 to 299 is OK
|
55
|
+
if not is_success:
|
56
|
+
self._logger.error(msg=log_line_post.format(False, response.status_code, response.reason))
|
57
|
+
raise OwnerrezApiException(f"{response.status_code}: {response.reason}")
|
58
|
+
|
59
|
+
# Try to parse the response as JSON only for successful responses
|
60
|
+
try:
|
61
|
+
data_out = response.json()
|
62
|
+
except (ValueError, TypeError, JSONDecodeError) as e:
|
63
|
+
self._logger.error(msg=log_line_post.format(False, response.status_code, e))
|
64
|
+
raise OwnerrezApiException("Bad JSON in response") from e
|
65
|
+
|
66
|
+
|
67
|
+
return Result(status=response.status_code, message=response.reason, data=data_out)
|
68
|
+
|
69
|
+
def get(self, endpoint: str, ep_params: Dict = None) -> Result:
|
70
|
+
"""
|
71
|
+
Make a GET request to the OwnerRez API
|
72
|
+
:param endpoint: API endpoint to call
|
73
|
+
:param ep_params: Dictionary of parameters to pass to the endpoint
|
74
|
+
:return: Result object
|
75
|
+
"""
|
76
|
+
return self._do_request(http_method='GET', endpoint=endpoint, ep_params=ep_params)
|
@@ -0,0 +1,187 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: ownerrez-wrapper
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python wrapper for the OwnerRez API
|
5
|
+
Home-page: https://github.com/gmoorevt/ownerrez-wrapper
|
6
|
+
Author: Geody Moore
|
7
|
+
Author-email: geody.moore@gmail.com
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Developers
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Requires-Python: >=3.10
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
License-File: LICENSE
|
19
|
+
Requires-Dist: requests>=2.25.0
|
20
|
+
Requires-Dist: python-dateutil>=2.8.0
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
24
|
+
Requires-Dist: pytest-cov>=2.0.0; extra == "dev"
|
25
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
26
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
27
|
+
Requires-Dist: flake8>=3.9.0; extra == "dev"
|
28
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
29
|
+
Requires-Dist: freezegun>=1.2.0; extra == "dev"
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: description
|
34
|
+
Dynamic: description-content-type
|
35
|
+
Dynamic: home-page
|
36
|
+
Dynamic: provides-extra
|
37
|
+
Dynamic: requires-dist
|
38
|
+
Dynamic: requires-python
|
39
|
+
Dynamic: summary
|
40
|
+
|
41
|
+
# OwnerRez API Wrapper
|
42
|
+
|
43
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
- Python 3.10 or higher
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
```bash
|
52
|
+
pip install ownerrez-wrapper
|
53
|
+
```
|
54
|
+
|
55
|
+
## Python Usage
|
56
|
+
|
57
|
+
```python
|
58
|
+
from ownerrez_wrapper import API
|
59
|
+
from datetime import datetime
|
60
|
+
|
61
|
+
# Initialize the API client
|
62
|
+
api = API(username="your_username", token="your_token")
|
63
|
+
|
64
|
+
# Get all properties
|
65
|
+
properties = api.getproperties()
|
66
|
+
for prop in properties:
|
67
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
68
|
+
|
69
|
+
# Get bookings for a property
|
70
|
+
bookings = api.getbookings(
|
71
|
+
property_id=123,
|
72
|
+
since_utc=datetime(2024, 1, 1)
|
73
|
+
)
|
74
|
+
for booking in bookings:
|
75
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
76
|
+
|
77
|
+
# Get a specific booking
|
78
|
+
booking = api.getbooking(booking_id=456)
|
79
|
+
print(f"Guest ID: {booking.guest_id}")
|
80
|
+
|
81
|
+
# Get guest details
|
82
|
+
guest = api.getguest(guest_id=789)
|
83
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
84
|
+
|
85
|
+
# Check if a property is currently booked
|
86
|
+
is_booked = api.isunitbooked(property_id=123)
|
87
|
+
print(f"Property is booked: {is_booked}")
|
88
|
+
```
|
89
|
+
|
90
|
+
## API Documentation
|
91
|
+
|
92
|
+
### API Class Methods
|
93
|
+
|
94
|
+
- `getproperties()` -> List[Property]
|
95
|
+
- Returns a list of all properties
|
96
|
+
|
97
|
+
- `getproperty(property_id: int)` -> Property
|
98
|
+
- Get details for a specific property
|
99
|
+
- Parameters:
|
100
|
+
- property_id: The property ID
|
101
|
+
|
102
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
103
|
+
- Get bookings for a specific property since a given date
|
104
|
+
- Parameters:
|
105
|
+
- property_id: The property ID
|
106
|
+
- since_utc: DateTime object for the start date
|
107
|
+
|
108
|
+
- `getbooking(booking_id: int)` -> Booking
|
109
|
+
- Get details for a specific booking
|
110
|
+
- Parameters:
|
111
|
+
- booking_id: The booking ID
|
112
|
+
|
113
|
+
- `getguest(guest_id: int)` -> Guest
|
114
|
+
- Get details for a specific guest
|
115
|
+
- Parameters:
|
116
|
+
- guest_id: The guest ID
|
117
|
+
|
118
|
+
- `isunitbooked(property_id: int)` -> bool
|
119
|
+
- Check if a property is currently booked
|
120
|
+
- Parameters:
|
121
|
+
- property_id: The property ID
|
122
|
+
|
123
|
+
### Data Models
|
124
|
+
|
125
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
126
|
+
|
127
|
+
Main models:
|
128
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
129
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
130
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
131
|
+
|
132
|
+
Supporting models:
|
133
|
+
- `Address`: Physical address information
|
134
|
+
- `EmailAddress`: Email contact information
|
135
|
+
- `Phone`: Phone contact information
|
136
|
+
- `Charge`: Booking charge information
|
137
|
+
- `DoorCode`: Property access codes
|
138
|
+
|
139
|
+
## CLI Tool (for testing)
|
140
|
+
|
141
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
# Using .env file
|
145
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
146
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
147
|
+
|
148
|
+
# List properties
|
149
|
+
ownerrez properties
|
150
|
+
|
151
|
+
# Get property details
|
152
|
+
ownerrez property 123
|
153
|
+
|
154
|
+
# Check bookings
|
155
|
+
ownerrez bookings 123 --since 2024-01-01
|
156
|
+
|
157
|
+
# Get booking details
|
158
|
+
ownerrez booking 456
|
159
|
+
```
|
160
|
+
|
161
|
+
## Development
|
162
|
+
|
163
|
+
To set up the development environment:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
167
|
+
cd ownerrez-wrapper
|
168
|
+
pip install -e ".[dev]"
|
169
|
+
```
|
170
|
+
|
171
|
+
To run tests:
|
172
|
+
|
173
|
+
```bash
|
174
|
+
pytest
|
175
|
+
```
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
180
|
+
|
181
|
+
## Author
|
182
|
+
|
183
|
+
- Geody Moore (geody.moore@gmail.com)
|
184
|
+
|
185
|
+
## Contributing
|
186
|
+
|
187
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
LICENSE
|
2
|
+
MANIFEST.in
|
3
|
+
README.md
|
4
|
+
requirements-dev.txt
|
5
|
+
requirements.txt
|
6
|
+
setup.py
|
7
|
+
ownerrez_wrapper/__init__.py
|
8
|
+
ownerrez_wrapper/api.py
|
9
|
+
ownerrez_wrapper/cli.py
|
10
|
+
ownerrez_wrapper/constants.py
|
11
|
+
ownerrez_wrapper/exceptions.py
|
12
|
+
ownerrez_wrapper/model.py
|
13
|
+
ownerrez_wrapper/restAdapter.py
|
14
|
+
ownerrez_wrapper.egg-info/PKG-INFO
|
15
|
+
ownerrez_wrapper.egg-info/SOURCES.txt
|
16
|
+
ownerrez_wrapper.egg-info/dependency_links.txt
|
17
|
+
ownerrez_wrapper.egg-info/entry_points.txt
|
18
|
+
ownerrez_wrapper.egg-info/requires.txt
|
19
|
+
ownerrez_wrapper.egg-info/top_level.txt
|
20
|
+
tests/__init__.py
|
21
|
+
tests/test_api.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from setuptools import setup, find_packages
|
2
|
+
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
4
|
+
long_description = fh.read()
|
5
|
+
|
6
|
+
setup(
|
7
|
+
name="ownerrez-wrapper",
|
8
|
+
version="0.1.0",
|
9
|
+
author="Geody Moore",
|
10
|
+
author_email="geody.moore@gmail.com",
|
11
|
+
description="A Python wrapper for the OwnerRez API",
|
12
|
+
long_description=long_description,
|
13
|
+
long_description_content_type="text/markdown",
|
14
|
+
url="https://github.com/gmoorevt/ownerrez-wrapper",
|
15
|
+
packages=find_packages(),
|
16
|
+
classifiers=[
|
17
|
+
"Development Status :: 3 - Alpha",
|
18
|
+
"Intended Audience :: Developers",
|
19
|
+
"License :: OSI Approved :: MIT License",
|
20
|
+
"Operating System :: OS Independent",
|
21
|
+
"Programming Language :: Python :: 3",
|
22
|
+
"Programming Language :: Python :: 3.10",
|
23
|
+
"Programming Language :: Python :: 3.11",
|
24
|
+
"Programming Language :: Python :: 3.12",
|
25
|
+
],
|
26
|
+
python_requires=">=3.10",
|
27
|
+
install_requires=[
|
28
|
+
"requests>=2.25.0",
|
29
|
+
"python-dateutil>=2.8.0",
|
30
|
+
"python-dotenv>=1.0.0"
|
31
|
+
],
|
32
|
+
extras_require={
|
33
|
+
"dev": [
|
34
|
+
"pytest>=6.0.0",
|
35
|
+
"pytest-cov>=2.0.0",
|
36
|
+
"black>=22.0.0",
|
37
|
+
"isort>=5.0.0",
|
38
|
+
"flake8>=3.9.0",
|
39
|
+
"mypy>=0.900",
|
40
|
+
"freezegun>=1.2.0"
|
41
|
+
]
|
42
|
+
},
|
43
|
+
entry_points={
|
44
|
+
'console_scripts': [
|
45
|
+
'ownerrez=ownerrez_wrapper.cli:main',
|
46
|
+
],
|
47
|
+
},
|
48
|
+
)
|
File without changes
|
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
Tests for the OwnerRez API wrapper
|
3
|
+
"""
|
4
|
+
import pytest
|
5
|
+
from datetime import datetime
|
6
|
+
from freezegun import freeze_time
|
7
|
+
from ownerrez_wrapper.api import API
|
8
|
+
from ownerrez_wrapper.model import Result, Property, Booking, Guest
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def api():
|
12
|
+
"""Create an API instance for testing"""
|
13
|
+
return API(username="test_user", token="test_token")
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def mock_property_response():
|
17
|
+
return {
|
18
|
+
"items": [{
|
19
|
+
"id": 1,
|
20
|
+
"name": "Test Property",
|
21
|
+
"bedrooms": 3,
|
22
|
+
"bathrooms": 2,
|
23
|
+
"active": True
|
24
|
+
}]
|
25
|
+
}
|
26
|
+
|
27
|
+
@pytest.fixture
|
28
|
+
def mock_booking_response():
|
29
|
+
return {
|
30
|
+
"items": [{
|
31
|
+
"id": 1,
|
32
|
+
"property_id": 1,
|
33
|
+
"arrival": "2024-03-01",
|
34
|
+
"departure": "2024-03-05",
|
35
|
+
"status": "confirmed",
|
36
|
+
"guest_id": 1
|
37
|
+
}]
|
38
|
+
}
|
39
|
+
|
40
|
+
@pytest.fixture
|
41
|
+
def mock_guest_response():
|
42
|
+
return {
|
43
|
+
"id": 1,
|
44
|
+
"first_name": "John",
|
45
|
+
"last_name": "Doe",
|
46
|
+
"email_addresses": [{"address": "john@example.com", "is_default": True}]
|
47
|
+
}
|
48
|
+
|
49
|
+
def test_api_initialization(api):
|
50
|
+
"""Test API initialization"""
|
51
|
+
assert api.username == "test_user"
|
52
|
+
assert api.token == "test_token"
|
53
|
+
|
54
|
+
def test_getproperties(api, mock_property_response, monkeypatch):
|
55
|
+
def mock_get(*args, **kwargs):
|
56
|
+
return Result(status=200, message="OK", data=mock_property_response)
|
57
|
+
|
58
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
59
|
+
|
60
|
+
properties = api.getproperties()
|
61
|
+
assert len(properties) == 1
|
62
|
+
assert isinstance(properties[0], Property)
|
63
|
+
assert properties[0].id == 1
|
64
|
+
assert properties[0].name == "Test Property"
|
65
|
+
|
66
|
+
def test_getbookings(api, mock_booking_response, monkeypatch):
|
67
|
+
def mock_get(*args, **kwargs):
|
68
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
69
|
+
|
70
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
71
|
+
|
72
|
+
bookings = api.getbookings(property_id=1, since_utc=datetime(2024, 1, 1))
|
73
|
+
assert len(bookings) == 1
|
74
|
+
assert isinstance(bookings[0], Booking)
|
75
|
+
assert bookings[0].id == 1
|
76
|
+
assert bookings[0].property_id == 1
|
77
|
+
|
78
|
+
def test_getbooking(api, monkeypatch):
|
79
|
+
booking_data = {
|
80
|
+
"id": 1,
|
81
|
+
"property_id": 1,
|
82
|
+
"arrival": "2024-03-01",
|
83
|
+
"departure": "2024-03-05"
|
84
|
+
}
|
85
|
+
|
86
|
+
def mock_get(*args, **kwargs):
|
87
|
+
return Result(status=200, message="OK", data=booking_data)
|
88
|
+
|
89
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
90
|
+
|
91
|
+
booking = api.getbooking(booking_id=1)
|
92
|
+
assert isinstance(booking, Booking)
|
93
|
+
assert booking.id == 1
|
94
|
+
assert booking.property_id == 1
|
95
|
+
|
96
|
+
def test_getguest(api, mock_guest_response, monkeypatch):
|
97
|
+
def mock_get(*args, **kwargs):
|
98
|
+
return Result(status=200, message="OK", data=mock_guest_response)
|
99
|
+
|
100
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
101
|
+
|
102
|
+
guest = api.getguest(guest_id=1)
|
103
|
+
assert isinstance(guest, Guest)
|
104
|
+
assert guest.id == 1
|
105
|
+
assert guest.first_name == "John"
|
106
|
+
assert guest.last_name == "Doe"
|
107
|
+
|
108
|
+
@freeze_time("2024-03-03") # A date between arrival and departure
|
109
|
+
def test_isunitbooked(api, mock_booking_response, monkeypatch):
|
110
|
+
def mock_get(*args, **kwargs):
|
111
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
112
|
+
|
113
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
114
|
+
is_booked = api.isunitbooked(property_id=1)
|
115
|
+
assert is_booked is True
|
116
|
+
|
117
|
+
@freeze_time("2024-03-06") # A date after departure
|
118
|
+
def test_isunitbooked_not_booked(api, mock_booking_response, monkeypatch):
|
119
|
+
def mock_get(*args, **kwargs):
|
120
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
121
|
+
|
122
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
123
|
+
is_booked = api.isunitbooked(property_id=1)
|
124
|
+
assert is_booked is False
|