Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.x changes #67

Open
wants to merge 16 commits into
base: 1.x
Choose a base branch
from
8 changes: 5 additions & 3 deletions .github/workflows/run-tests.yml
Expand Up @@ -18,23 +18,23 @@ jobs:
strategy:
matrix:
python-version:
- 3.8
- 3.9
- '3.10'
- 3.11
- 3.12
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Create docker-compose.yml
run: curl https://raw.githubusercontent.com/farmOS/farmOS/2.x/docker/docker-compose.development.yml -o docker-compose.yml
run: curl https://raw.githubusercontent.com/farmOS/farmOS/3.x/docker/docker-compose.development.yml -o docker-compose.yml
- name: Start containers
run: docker-compose up -d && sleep 5
- name: Install farmOS
run: |
docker-compose exec -u www-data -T www drush site-install -y --db-url=pgsql://farm:farm@db/farm --account-pass=admin
docker-compose exec -u www-data -T www drush en farm_api_default_consumer -y
docker-compose exec -u www-data -T www drush user-create tester --password test
docker-compose exec -u www-data -T www drush user-add-role farm_manager tester
docker-compose exec -u www-data -T www drush config:set simple_oauth.settings access_token_expiration 15 -y
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
Expand All @@ -43,6 +43,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pytest -e .[test]
- name: Check unasync
run: python farmOS/utils/unasync.py --check
- name: Run farmOS.py tests.
run: pytest tests
env:
Expand Down
158 changes: 3 additions & 155 deletions farmOS/__init__.py
@@ -1,156 +1,4 @@
import logging
from datetime import datetime
from functools import partial
from urllib.parse import urlparse, urlunparse
from ._async.client import AsyncFarmClient
from ._sync.client import FarmClient

from . import client, client_2, subrequests
from .session import OAuthSession

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())


class farmOS:
"""A client that connects to the farmOS server."""

def __init__(
self,
hostname,
client_id="farm",
client_secret=None,
scope="farm_manager",
token=None,
token_updater=lambda new_token: None,
version=2,
):
"""
Initialize instance of the farmOS client that connects to a single farmOS server.

:param hostname: Valid hostname without a path or query params. The HTTPS scheme
will be added if none is specified.
:param client_id: OAuth Client ID. Defaults to "farm"
:param client_secret: OAuth Client Secret. Defaults to None.
:param scope: OAuth Scope. Defaults to "farm_manager".
:param token: An existing OAuth token to use.
:param token_updater: A function used to save OAuth tokens outside of the client.
:param version: The major version of the farmOS server. Defaults to 2.
"""

logger.debug("Creating farmOS client.")

# Save the token_updater function.
self.token_updater = token_updater

# Save the session.
self.session = None

if hostname is not None:
valid_schemes = ["http", "https"]
default_scheme = "https"
parsed_url = urlparse(hostname)

# Validate the hostname.
# Add a default scheme if not provided.
if not parsed_url.scheme:
parsed_url = parsed_url._replace(scheme=default_scheme)
logger.debug("No scheme provided. Using %s", default_scheme)

# Check for a valid scheme.
if parsed_url.scheme not in valid_schemes:
raise Exception("Not a valid scheme.")

# If not netloc was provided, it was probably parsed as the path.
if not parsed_url.netloc and parsed_url.path:
parsed_url = parsed_url._replace(netloc=parsed_url.path)
parsed_url = parsed_url._replace(path="")

# Check for netloc.
if not parsed_url.netloc:
raise Exception("Invalid hostname. Must have netloc.")

# Don't allow path, params, or query.
if parsed_url.path or parsed_url.params or parsed_url.query:
raise Exception("Hostname cannot include path and query parameters.")

# Build the url again to include changes.
hostname = urlunparse(parsed_url)
logger.debug("Complete hostname configured as %s", hostname)

else:
raise Exception("No hostname provided and could not be loaded from config.")

logger.debug("Creating an OAuth Session.")
# OR implement a method to check both token paths.
# maybe version can default to none, and check the server?
token_url = hostname + "/oauth/token"

# Check the token expiration time.
if token is not None and "expires_at" in token:
# Create datetime objects for comparison.
now = datetime.now()
expiration_time = datetime.fromtimestamp(float(token["expires_at"]))

# Calculate seconds until expiration.
timedelta = expiration_time - now
expires_in = timedelta.total_seconds()

# Update the token expires_in value
token["expires_in"] = expires_in

# Unset the 'expires_at' key.
token.pop("expires_at")

# Determine the Content-Type header depending on server version.
content_type = (
"application/vnd.api+json" if version == 2 else "application/json"
)

# Create an OAuth Session
self.session = OAuthSession(
hostname=hostname,
client_id=client_id,
client_secret=client_secret,
scope=scope,
token=token,
token_url=token_url,
content_type=content_type,
token_updater=self.token_updater,
)

self._client_id = client_id
self._client_secret = client_secret

if self.session is None:
raise Exception(
"Could not create a session object. Supply authentication credentials when "
"initializing a farmOS Client."
)

if version == 2:
self.log = client_2.LogAPI(self.session)
self.asset = client_2.AssetAPI(self.session)
self.term = client_2.TermAPI(self.session)
self.resource = client_2.ResourceBase(self.session)
self.info = partial(client_2.info, self.session)
self.subrequests = subrequests.SubrequestsBase(self.session)
self.filter = client_2.filter
else:
self.log = client.LogAPI(self.session)
self.asset = client.AssetAPI(self.session)
self.area = client.AreaAPI(self.session)
self.term = client.TermAPI(self.session)
self.info = partial(client.info, self.session)

def authorize(self, username=None, password=None, scope=None):
"""Authorize with the farmOS server.

The client must be authorized with the farmOS server before making requests.
This method utilizes the OAuth Password Credentials flow to authorize users.

:param username: farmOS Username. Prompted if not included.
:param password: farmOS Password. Prompted if not included.
:param scope: Scope to authorize as with the farmOS server. Defaults to "farm_manager".
:return: OAuth Token.
"""

return self.session.authorize(username, password, scope)
__all__ = ["AsyncFarmClient", "FarmClient"]
Empty file added farmOS/_async/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions farmOS/_async/client.py
@@ -0,0 +1,18 @@
from functools import partial

from httpx import AsyncClient

from farmOS._async import resource, subrequests
from farmOS.filter import filter


class AsyncFarmClient(AsyncClient):
def __init__(self, hostname, **kwargs):
super().__init__(base_url=hostname, **kwargs)
self.info = partial(resource.info, self)
self.filter = filter
self.subrequests = subrequests.SubrequestsBase(self)
self.resource = resource.ResourceBase(self)
self.log = resource.LogAPI(self)
self.asset = resource.AssetAPI(self)
self.term = resource.TermAPI(self)